The Learning Harness

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

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.

PropertyValue
$process_person_profileAlways false. PostHog makes no person profile.
$geoip_disableAlways true. PostHog is asked not to look up a location from the network address.
$libAlways learning-harness.
app_buildThe app's build number.
app_versionThe app's version.
environmentproduction, or test for events that only prove the system works.
os_majorThe major version of macOS, for example 26.
schema_versionThe 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.

EventFamilyBudgetSent whenFieldsOptional fields
setup_invite_shownactivationordinaryThe invite code screen shows.nonenone
setup_invite_acceptedactivationordinaryThe app accepts your invite code.nonenone
setup_welcome_shownactivationordinaryThe welcome screen shows.nonenone
setup_sign_in_startedactivationordinaryYou press the main action on the welcome screen.nonenone
setup_sign_in_finishedactivationordinaryYou are signed in and the setup checks pass.nonenone
setup_about_you_shownactivationordinaryThe optional card about you shows.nonenone
setup_about_you_finishedactivationordinaryYou finish or skip the card about you.nonenone
setup_finishedactivationordinarySetup ends and the chat opens.nonenone
first_goal_sentactivationordinaryYou send your first goal from an empty chat.nonenone
app_openedlearning_flowordinaryThe app starts.nonenone
message_sentlearning_flowordinaryYou send a chat message, or use Try again or Resume.nonenone
answer_sentlearning_flowordinaryYou answer a card, and the tutor goes on.nonenone
plan_shownlearning_flowordinaryThe app shows a Plan card.nonenone
lesson_startedlearning_flowordinaryThe tutor starts a lesson.nonenone
lesson_step_startedlearning_flowordinaryYou start a lesson or go on with a step of the Plan.nonenone
file_attachedlearning_flowordinaryYou add a file to a message. Its name and content are never sent.nonenone
plan_acceptedactivationordinaryYou commit to a Plan for the first time after you turned sharing on.nonenone
first_lesson_startedactivationordinaryThe first lesson starts after you turned sharing on.nonenone
wait_endedwait_outcomeordinaryA wait for the tutor ends.duration_seconds, outcome, work_categorynone
error_occurredreliabilityordinaryA part of the app fails. Only a fixed code is sent.error_codenone
lesson_failedreliabilityordinaryA lesson cannot be made.nonenone
answer_delivery_failedreliabilityordinaryThe app could not confirm that an answer reached the tutor.nonenone
visual_usedvisual_useordinaryA visual is shown, used, expanded, reset, or cannot be drawn.visual_kind, visual_actionnone
practice_offeredlearning_flowordinaryThe app offers you extra practice.noneitem_kind
review_startedlearning_flowordinaryYou start a flashcard review.noneitem_kind
review_completedlearning_flowordinaryYou finish a flashcard review.noneitem_kind
hint_requestedlearning_flowordinaryYou ask for a hint.noneitem_kind
mascot_summarymascot_summarysummaryEvery 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, coveragenone
environment_snapshotcompatibilityordinaryYou turn sharing on, and later when the window size or zoom moves to another broad bucket and settles.window_width, window_height, zoomnone
collection_healthcollection_healthhealthRecords were lost on your Mac since the last count.dropped_eventsnone

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.

FieldAllowed values
duration_secondsA number from 0 to 86400 seconds, rounded to a tenth.
outcomecompleted, stopped, failed
work_categoryreply, lesson, check, other
error_codesign_in_failed, engine_start_failed, engine_stopped, study_folder_unavailable, learning_tools_failed, flashcards_unavailable, network_unreachable, unknown
visual_kindlive_diagram, pele_diagram, interface, math
visual_actionpresented, interacted, expanded, reset, render_failed
item_kindfact, concept, ability
wait_secondsA 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_secondsA number from 0 to 3600 seconds, rounded to a tenth. Seconds of those in which the mascot's pose changed.
outside_wait_secondsA 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_secondsA number from 0 to 3600 seconds, rounded to a tenth. Seconds of those in which the mascot's pose changed.
unobserved_wait_secondsA 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_secondsA number from 0 to 3600 seconds, rounded to a tenth. Seconds outside a wait in which no mascot could be seen.
metric_versionA whole number from 1 to 100. It names how the pose change was counted.
coveragecomplete, partial
dropped_eventsA whole number from 1 to 100000. How many records were lost on this Mac.
window_widthunder_900, 900_1299, 1300_1699, 1700_plus (points)
window_heightunder_600, 600_899, 900_plus (points)
zoomunder_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.

CategoryThe action that started the wait
replyYou sent a chat message, or used Try again or Resume.
lessonYou started a lesson or went on with a step of the Plan.
checkYou answered a card (a question or the Plan card), and the tutor went on.
otherAnything 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

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.

BudgetRecords per hourCounts
ordinary300Every event except the three below.
summary12The periodic mascot_summary, one for every five minutes.
remainder6The shorter mascot_summary at the end of a window or the app.
health4collection_health.
LimitValue
queue_ordinary100
queue_reserved24
batch_size20
attempts3
retry_delays_seconds2, 8
flush_delay_seconds30
summary_period_seconds300

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.