Usage statistics
What the app sends while sharing is on, to whom, and what it never sends.
Learning Harness sends usage statistics that exclude your learning content. A new installation shares them by default. You can turn this off at any time.
Your choice
- A new installation shares usage statistics from its first launch. The first screen says so, and it has "Turn off" and "What is shared". Setup asks no question about it.
- A saved answer stays. If you chose "Don't share", or closed the question in an earlier version, sharing stays off after an update, a restart, another study folder or a Full reset. Version 0.16.2 gave each installation one fresh start, which cleared the saved answer, so that installation answered again at the end of setup.
- Settings has the same choice.
- No feature of the app depends on your choice.
- You can change your choice in Settings, General, at any time. Settings always shows the current state.
- A build that cannot send (see "Which builds can send") shows "Usage statistics are unavailable in this build." and no control.
Who receives it
PostHog (PostHog Inc.), on its US cloud, at https://us.i.posthog.com/batch/. PostHog's privacy information is at https://posthog.com/privacy. The app sends to that exact address only. It follows no redirect and it accepts no other host, subdomain or region.
Your Mac connects to PostHog over HTTPS. While a request arrives, PostHog's servers and the networks in between can see the network address of your Mac. No program on your Mac can hide that. The project (PostHog project ID 698375, US cloud) has the setting "Discard client IP data" on, so PostHog discards the client IP address. That setting is applied when an event is processed; for that reason every event also says that PostHog must not look up a location ($geoip_disable), because a lookup can run before the address is discarded.
Why
To find out where new learners stop in setup, what they use, whether features work, how long waits for the tutor are, which technical errors happen, whether the app fits on the screens of its users, and whether the mascot is seen during waits. These statistics measure how the product operates and whether practice is reachable. They do not measure what you learned, and they must not be read as a measure of learning.
What is sent
Every event has the properties in the first table, and the fields of its own event in the second table. There is no other property. The app sets all of them; none comes from what you type or read.
| Property | Value |
|---|---|
$process_person_profile | Always false. PostHog makes no person profile. |
$geoip_disable | Always true. PostHog is asked not to look up a location from the network address. |
$lib | Always learning-harness. |
app_build | The app's build number. |
app_version | The app's version. |
environment | production, or test for events that only prove the system works. |
os_major | The major version of macOS, for example 26. |
schema_version | The version of this schema. It is 2 now. |
Each event also has its name, a random code (see "The random code"), the time to the second, and a unique event identifier. A repeated delivery of the same event carries the same identifier, so that PostHog can discard the repeat.
| Event | Family | Budget | Sent when | Fields | Optional fields |
|---|---|---|---|---|---|
setup_invite_shown | activation | ordinary | The invite code screen shows. | none | none |
setup_invite_accepted | activation | ordinary | The app accepts your invite code. | none | none |
setup_welcome_shown | activation | ordinary | The welcome screen shows. | none | none |
setup_sign_in_started | activation | ordinary | You press the main action on the welcome screen. | none | none |
setup_sign_in_finished | activation | ordinary | You are signed in and the setup checks pass. | none | none |
setup_about_you_shown | activation | ordinary | The optional card about you shows. | none | none |
setup_about_you_finished | activation | ordinary | You finish or skip the card about you. | none | none |
setup_finished | activation | ordinary | Setup ends and the chat opens. | none | none |
first_goal_sent | activation | ordinary | You send your first goal from an empty chat. | none | none |
app_opened | learning_flow | ordinary | The app starts. | none | none |
message_sent | learning_flow | ordinary | You send a chat message, or use Try again or Resume. | none | none |
answer_sent | learning_flow | ordinary | You answer a card, and the tutor goes on. | none | none |
plan_shown | learning_flow | ordinary | The app shows a Plan card. | none | none |
lesson_started | learning_flow | ordinary | The tutor starts a lesson. | none | none |
lesson_step_started | learning_flow | ordinary | You start a lesson or go on with a step of the Plan. | none | none |
file_attached | learning_flow | ordinary | You add a file to a message. Its name and content are never sent. | none | none |
plan_accepted | activation | ordinary | You commit to a Plan for the first time after you turned sharing on. | none | none |
first_lesson_started | activation | ordinary | The first lesson starts after you turned sharing on. | none | none |
wait_ended | wait_outcome | ordinary | A wait for the tutor ends. | duration_seconds, outcome, work_category | none |
error_occurred | reliability | ordinary | A part of the app fails. Only a fixed code is sent. | error_code | none |
lesson_failed | reliability | ordinary | A lesson cannot be made. | none | none |
answer_delivery_failed | reliability | ordinary | The app could not confirm that an answer reached the tutor. | none | none |
visual_used | visual_use | ordinary | A visual is shown, used, expanded, reset, or cannot be drawn. | visual_kind, visual_action | none |
practice_offered | learning_flow | ordinary | The app offers you extra practice. | none | item_kind |
review_started | learning_flow | ordinary | You start a flashcard review. | none | item_kind |
review_completed | learning_flow | ordinary | You finish a flashcard review. | none | item_kind |
hint_requested | learning_flow | ordinary | You ask for a hint. | none | item_kind |
mascot_summary | mascot_summary | summary | Every five minutes while the app runs, and for the rest of a period when the window closes or the app quits. | wait_seconds, wait_pose_change_seconds, outside_wait_seconds, outside_wait_pose_change_seconds, unobserved_wait_seconds, unobserved_outside_wait_seconds, metric_version, coverage | none |
environment_snapshot | compatibility | ordinary | You turn sharing on, and later when the window size or zoom moves to another broad bucket and settles. | window_width, window_height, zoom | none |
collection_health | collection_health | health | Records were lost on your Mac since the last count. | dropped_events | none |
A field is a number in a range or a word from a closed list. There is no text field. An optional field is sent only when it is true: item_kind is sent only when the event concerns exactly one fact, one concept or one ability, and a review of mixed items omits it.
| Field | Allowed values |
|---|---|
duration_seconds | A number from 0 to 86400 seconds, rounded to a tenth. |
outcome | completed, stopped, failed |
work_category | reply, lesson, check, other |
error_code | sign_in_failed, engine_start_failed, engine_stopped, study_folder_unavailable, learning_tools_failed, flashcards_unavailable, network_unreachable, unknown |
visual_kind | live_diagram, pele_diagram, interface, math |
visual_action | presented, interacted, expanded, reset, render_failed |
item_kind | fact, concept, ability |
wait_seconds | A number from 0 to 3600 seconds, rounded to a tenth. Seconds of the period in which a mascot was seen while the app waited for the tutor. |
wait_pose_change_seconds | A number from 0 to 3600 seconds, rounded to a tenth. Seconds of those in which the mascot's pose changed. |
outside_wait_seconds | A number from 0 to 3600 seconds, rounded to a tenth. Seconds of the period in which a mascot was seen outside a wait. |
outside_wait_pose_change_seconds | A number from 0 to 3600 seconds, rounded to a tenth. Seconds of those in which the mascot's pose changed. |
unobserved_wait_seconds | A number from 0 to 3600 seconds, rounded to a tenth. Seconds of waiting in which no mascot could be seen (the window was hidden or no mascot was on screen). |
unobserved_outside_wait_seconds | A number from 0 to 3600 seconds, rounded to a tenth. Seconds outside a wait in which no mascot could be seen. |
metric_version | A whole number from 1 to 100. It names how the pose change was counted. |
coverage | complete, partial |
dropped_events | A whole number from 1 to 100000. How many records were lost on this Mac. |
window_width | under_900, 900_1299, 1300_1699, 1700_plus (points) |
window_height | under_600, 600_899, 900_plus (points) |
zoom | under_100, 100_124, 125_134, 135_plus (percent) |
How the numbers are made
Setup. Each setup_ event and first_goal_sent is sent once for one random code. It says that a step was reached, and nothing about what you typed or chose. A step that you never reach sends nothing, which is how a stop in setup can be seen.
Usage. message_sent, answer_sent, plan_shown, lesson_started, lesson_step_started and file_attached say only that the action happened. app_opened is sent once for one launch of the app.
Activation. first_lesson_started is sent when the tutor starts a lesson in the chat that is open, after you turned sharing on. A lesson that began before you turned sharing on, a chat that the app restores, a late first read of the study state, and a lesson that leaves the state and comes back are not starts. The event does not carry the lesson or the time it began.
Waits. A wait starts when the tutor begins to work on something that you asked for, and ends when the tutor needs you (a question card), you stop it, it finishes, or it fails. A question that waits for you is not a wait for the tutor. The category follows the action that you took, and it does not change when the tutor uses other tools in the same wait.
| Category | The action that started the wait |
|---|---|
reply | You sent a chat message, or used Try again or Resume. |
lesson | You started a lesson or went on with a step of the Plan. |
check | You answered a card (a question or the Plan card), and the tutor went on. |
other | Anything else: the app's own continuation of an interrupted reply, a message sent after a sign-in, a wait that you did not start. |
The outcome is completed when the reply finished or the tutor asked you something, stopped when you pressed Stop (this includes a cancel), and failed when the reply failed or the tutor's engine stopped without your request. Quitting the app ends no wait: a wait that was open at quit is not reported, and the app does not call it completed. The duration is measured with the Mac's monotonic clock, which does not count sleep.
Visuals. visual_kind names the part of the app that drew the visual (live_diagram, pele_diagram, interface). It does not say what kind of representation the visual is, it does not say what the visual shows, and interface does not mean that you learned by interacting. math is in the list for a later renderer and is not sent by this version. presented means that the content was shown and was more than returned by a tool; a remount of the same visual is not presented again. interacted means that you did something with it; a replay or a restored state is not an interaction, and repeated interactions with one visual in one session count once. The app never sends which visual it was.
Errors. error_occurred carries one fixed code and nothing else. The code says what failed: the sign-in (sign_in_failed); the tutor's engine, which did not start (engine_start_failed) or stopped without your request (engine_stopped); the study folder or the learning tools (study_folder_unavailable, learning_tools_failed); the flashcards (flashcards_unavailable); the network (network_unreachable); or a cause that is not known (unknown). A reply that fails because of your plan, the model, a usage limit or credits is not a fault of the app and is not sent. The same code is sent at most once a minute, so a failure that repeats on every check is one event. lesson_failed is sent when a lesson that you asked for cannot be made, and answer_delivery_failed when the app could not confirm that an answer reached the tutor. The app cannot tell whether the tutor received the answer anyway, so this event does not say that the answer was lost or saved. It is not sent when you stop or cancel a question. Neither event carries a reason.
Reviews and hints. review_started is sent in one of two ways, and the policy keeps them apart. When you open a flashcard review yourself, it is sent at once, even if you then rate nothing. When the app opens a review by itself, it is sent only when the app accepts your first rating, so a review that nobody answers is not counted. The count of review_started therefore mixes the two ways; it is not a count of reviews with a rating. review_completed is sent when the review ends in its normal way after the app accepted at least one rating; a review that you stop, skip or leave is not completed. hint_requested is sent when you press "Need a hint?" on a question and the app accepted the request for delivery. It does not say that a hint arrived or that you used it. None of the three says which fact, concept or ability it was (a review can mix them), how you rated or answered, or how many flashcards there were. They show how the app is used. They are not evidence of how well you remember or learn. practice_offered is in the list for a later screen and is not sent by this version.
The mascot. Every five minutes the app adds up, on your Mac, how many seconds passed in each of four places: a mascot was seen while waiting for the tutor, a mascot was seen outside a wait, no mascot could be seen while waiting, no mascot could be seen outside a wait. "Seen" means that the main window was shown and a mascot was on screen whose pose the app can read. A tutor figure that the app shows from a motion package, and whose pose the app cannot read, counts as not seen. For the seen seconds, the app also adds up the seconds in which the mascot's pose changed (its body, head and arm; not blinking, gaze or mouth). The periods do not overlap, a wait that crosses the end of a period is split there, and the unobserved seconds are sent, so that the seen seconds cannot shrink without notice. Sleep is not counted. The last, shorter period is sent when the window closes or the app quits, if the app can still send it; nothing is sent when you turn sharing off.
metric_version 1 is a runtime proxy: the seconds in which the app's own mascot code changed the pose. It is not a measure of motion that you saw, and it does not match any frame-by-frame measurement.
Windows. The window size and the zoom are sent in broad buckets when you turn sharing on, and again when a bucket changes and stays changed. Nothing is sent before the main window exists, and nothing is sent about the past.
What is never sent
Chats, answers, goals, topics, titles, prompts, learning materials, source links, file paths, names, email addresses, credentials, audio, images, video, clipboard content, tool names, tool arguments or results, crash stacks and error messages. The reports that the app saves on your Mac, and the problem report that you can create in Settings, are never sent by this feature. No grades and no estimate of what you know are sent: how often you use the app is not how well you learn.
The app does not use PostHog's software library, automatic capture, screen recording, surveys, feature flags, experiments, logs or crash reports. It does not identify or alias you, and it does not read a PostHog setting from the network. Nothing is collected, counted, queued or sent while sharing is off.
The random code
When sharing starts, the app makes a random code (an installation identifier, a UUID). PostHog uses it to see that several events come from the same installation. It is a pseudonymous identifier, not an anonymous one. It is not made from your email, your ChatGPT sign-in, your Mac's serial number, your study folder or any content, and it is not the identifier of the problem report.
When you turn sharing off, the app deletes the code. If you turn sharing on again, the app makes a new one, and nothing links the new code to the old one. Events from the time between are not collected.
When you turn sharing off
The app stops at once. It deletes the events that were not sent yet and the totals it was adding up, it deletes the code, and it cancels any retry. It does not send a last event, and it does not send what happened while sharing was off. A request that is already on its way cannot be called back. The events wait in memory only and are never written to disk, so quitting the app also removes them.
Statistics that PostHog already received stay there (see "Retention and deletion").
Full reset
The Full reset of the app keeps your choice (shared or not shared), whether the one ask was used, and which activation events (plan_accepted, first_lesson_started) were already sent. It deletes the code, the events that were not sent and the running totals. If sharing is on, the app makes a new code when it next records something; the new code is not linked to the old one, and no reset event is sent. A reset does not send an activation event again. Turning sharing off and on again is a separate rule: it starts a new code and sends each activation event once more.
Retention and deletion
On the plan that the project uses (free, checked 28 October 2026), PostHog keeps events for one year. This period cannot be shortened on request. A retention period is not a deletion tool: the project does not promise that data is physically removed when the period ends. The project does not offer, and does not promise, a way to delete the events of one installation. Whether the provider has such a route for events without a person profile has not been checked, so this policy makes no claim about it. Turning sharing off stops new statistics and clears the ones waiting on your Mac. It does not delete statistics that were already sent.
Gaps and limits
The statistics come only from installations where sharing is on, and only from events that were delivered. They cannot describe all learners. Events can be lost: the queue is in memory and bounded, a request can fail, the Mac can be offline, and the app can quit before it sends the last events. The app counts what it loses, per reason, on your Mac only, and it sends one number (collection_health, dropped_events) when it lost anything, so that the affected period is labelled incomplete. A mascot summary says partial when it contains seconds that were not observed, and it says how many.
Which builds can send
- A build without a project token sends nothing and does not ask. This includes a build that someone else makes from the source. The token is not in the repository. The release build (
LH_RELEASE_CLEAN=1 make build,scripts/build-release.py) reads it from the macOS Keychain (servicelearning-harness-posthog-token, accountrelease) and passes it as the build settingLH_TELEMETRY_TOKEN, withLH_TELEMETRY_HOST=https://us.i.posthog.comandLH_TELEMETRY_ENVIRONMENT=production. It never prints the token. A DEV build or a test build is not stamped, and a Keychain without the item leaves the build silent. It is a public ingestion token: it can be read from the app, and it gives no access to the project. - The signed Release app sends as
production. - Every other build (DEV, Lab, tests) sends only when it is built as a test build (
LH_TELEMETRY_ENVIRONMENT=test). It then marks every eventenvironment=test, and the project's dashboards leave those events out. The mark labels the events; it does not keep them in a separate project. - Only
https://us.i.posthog.com/batch/is accepted as the destination, and the host in the build setting must be exactlyhttps://us.i.posthog.com.
The limits in the code
Starting points for the engineering, not measured constants. Each budget counts per sliding hour of the Mac's monotonic clock; a record over its budget is lost and counted. The summaries and the health count have room of their own, so that a busy hour of ordinary events cannot take their place.
| Budget | Records per hour | Counts |
|---|---|---|
ordinary | 300 | Every event except the three below. |
summary | 12 | The periodic mascot_summary, one for every five minutes. |
remainder | 6 | The shorter mascot_summary at the end of a window or the app. |
health | 4 | collection_health. |
| Limit | Value |
|---|---|
queue_ordinary | 100 |
queue_reserved | 24 |
batch_size | 20 |
attempts | 3 |
retry_delays_seconds | 2, 8 |
flush_delay_seconds | 30 |
summary_period_seconds | 300 |
queue_ordinary is how many ordinary records wait in memory (the oldest goes first when it is full). queue_reserved is the room that summaries, remainders and the health count share. batch_size is the most records in one request. A failed request is tried attempts times, with the retry_delays_seconds between tries and with the same event identifier and bytes each time, and then dropped. flush_delay_seconds is how long the first waiting record waits before the automatic send. A failure of the statistics never stops the app: nothing in the app waits for them.
Contact
Questions about usage statistics: hello@learningharness.org.