GitHub Star History Is a Bucket Contract, Not an Event Feed
GitHub's privacy-safe API returns weekly buckets, not stargazer events. Handle pagination, partial weeks, churn, and attribution explicitly.
GitHub restored a useful growth signal this week without restoring the people behind it.
On 4 September, GitHub announced a privacy-safe repository star history endpoint. It returns historical star counts and timestamps without exposing stargazer identities. That is the replacement path for tools affected when GitHub restricted public stargazer and watcher lists after those lists were used to collect user data for spam.
The new route is not a redacted version of the old list. It is a different data product. Instead of one actor and one starred_at value per row, it returns one calendar week with a weekly total and seven daily counts. That preserves trend analysis while deliberately removing user-level enrichment, outreach, deduplication, and attribution paths.
The repeated angle to avoid
The ten most recent posts here covered Shopify POS resolver boundaries, browser-test topology, npm release authority, regional SEO enforcement, AI review approvals, private cache encodings, product-variant graphs, htmx state, Shopify finance clocks, and verified-bot policy. Older overlapping posts examined soft-navigation measurement discontinuities, repository-level Copilot outcomes, and bot-contaminated web analytics.
The weak version of this article would revive the old X needs Y formula: open-source projects need better star analytics. It would also repeat the generic warning that stars are not customers.
The sharper thesis is that GitHub changed the grain and capability of the dataset. An actor event can support identity joins. A calendar bucket can support trend calculations. A current count can support reconciliation. None can silently substitute for the others. Migrating a star-tracking utility therefore means deleting unsupported product capabilities as deliberately as adding the new endpoint.
The information surplus is the operating contract between those surfaces: how to page a reverse-chronological series, distinguish weekly totals from cumulative counts, handle a still-open week and non-UTC boundaries, preserve snapshots when churn semantics are not explicit, and evaluate a launch without pretending a daily aggregate proves causation.
Fresh evidence and background serve different jobs
| Source | Freshness | What it contributes |
|---|---|---|
| Privacy-safe star history announcement | 4 September 2026 | The product purpose: recover repository star-growth analysis without exposing individual stargazers |
| GitHub OpenAPI addition | 3 September 2026 | The route, schema, ordering, zero-week behaviour, limits, response codes, and GitHub App availability |
| REST documentation for starring | Current primary contract | Authentication, Metadata permission, public access, field meanings, page direction, and the warning that bucket boundaries may not align with UTC |
| Public stargazer access restrictions | Background, 30 June 2026 | Why public actor lists were restricted and which identity-bearing endpoints were affected |
| REST pagination guide | Current background documentation | Following Link relations and verifying endpoint-specific per_page limits |
The announcement and OpenAPI commit are the two fresh primary sources. The June restriction explains the privacy boundary; it is not evidence that every third-party star tool has already migrated.
One star metric now has three grains
The word “stars” can describe three incompatible records.
| Grain | Shape | Good question | Unsupported shortcut |
|---|---|---|---|
| Current count | One repository, one count now | How many current stargazers does this repository report? | How many people have ever starred it? |
| History bucket | One week, one total, seven day slots | When are the stars represented by this series concentrated? | Which person starred, or why? |
| Stargazer event | One actor and, with the relevant representation, one timestamp | Which accessible account starred and when? | Whether that account installed, used, bought, or recommended the product |
The new endpoint is:
GET /repos/{owner}/{repo}/stargazers/history
Each row has this shape:
{
"week": 1754784000,
"total": 19,
"days": [0, 12, 7, 0, 0, 0, 0]
}
week is a Unix timestamp for the start of the bucket. total is the number attributed to that week, not the repository's cumulative total. days starts on Sunday and contains seven daily counts. In the documented example, the weekly total is the sum of the seven slots.
A charting adapter that maps total directly to “stars to date” will produce a weekly acquisition chart while labelling it as a cumulative growth curve. To derive a cumulative line, order complete buckets from oldest to newest and calculate a running sum. Keep the original weekly totals as well; they are easier to audit and less likely to conceal a missing page.
Pagination is part of the data model
GitHub returns the most recent weeks first. Rows within a page are newest to oldest, and later pages move backwards towards repository creation. GitHub says concatenating pages produces one continuous reverse-chronological series, including weeks with zero stars.
Two limits matter:
per_pagehas a maximum of 30 for this endpoint, not the 100 common to many GitHub routes;pagehas a maximum of 100.
Always request 30 rows per page and follow the response's Link relations until there is no rel="next". A generic client that requests per_page=100 may be silently reduced to the endpoint maximum. A client that hard-codes ten pages will truncate repositories with more than 300 weeks of history. A client that requests only three rows per page can hit the 100-page ceiling before reaching an older repository's first bucket.
The practical ingestion key is not array position. Upsert by repository plus week, because every new week pushes older rows to a later page:
repository_star_bucket:
repository_id: stable_github_repository_id
week_key: github_week_unix_timestamp
total: 19
days_sunday_first:
- 0
- 12
- 7
- 0
- 0
- 0
- 0
observed_at: ingestion_timestamp
api_version: '2026-03-10'
source: github_star_history
Preserve the stable repository ID alongside owner/name. A transfer or rename should not split one history into two products merely because the path changed.
Run these assertions before promoting a fetch:
def validate_star_history(rows):
assert all(len(row["days"]) == 7 for row in rows)
assert all(row["total"] == sum(row["days"]) for row in rows)
assert all(
newer["week"] > older["week"]
for newer, older in zip(rows, rows[1:])
)
assert len({row["week"] for row in rows}) == len(rows)
Then compare the sum of fetched buckets with the separately fetched current count and record the difference. Do not discard the import just because the values differ: first distinguish an incomplete fetch, a snapshot taken at another time, and semantics that the public contract does not promise.
The open week is not comparable with a closed week
The first row is normally the current calendar week. On Monday it may contain two populated day slots and five future zeros. The next row contains seven completed days. Comparing their raw totals creates a predictable false decline every week.
Choose one of three display rules:
- Closed-week reporting: omit the newest bucket until the next bucket appears. This is the safest default for weekly trend and release reporting.
- Week-to-date reporting: compare only the same number of day slots from the prior week and label the comparison as partial.
- Daily reporting: display populated daily slots, but do not assign your own UTC dates unless you have proved how GitHub's bucket boundary maps to them.
The third rule matters because GitHub explicitly says its week and day boundaries are not guaranteed to align with UTC. The Unix value identifies GitHub's week bucket; adding exactly 24 hours to manufacture seven UTC dates can attach a release, campaign, or incident to the wrong displayed day around the provider's boundary.
Keep week and the day index as source identities. If the interface needs local calendar labels, describe them as presentation labels and retain the raw key for joins and corrections.
This is an aggregate snapshot, not an immutable ledger
The current-count documentation explicitly says users who removed their star are excluded from that count. The history documentation describes stars created during each week, but it does not explicitly promise whether a later unstar leaves the historical bucket unchanged or revises it.
That omission should change storage design.
During a 7 September probe, the sum of every returned weekly bucket matched the current stargazers_count for each of three public repositories tested: Vite, GitHub Spec Kit, and octocat/Hello-World. Every weekly total also matched the sum of its seven daily slots. That is a useful reconciliation observation, not a contractual guarantee that every repository will match at every instant or that historical rows never change.
If the series is needed for reporting, retain observations rather than overwriting the only copy:
repository_star_history_observation:
repository_id: stable_github_repository_id
observed_at: ingestion_timestamp
request_api_version: '2026-03-10'
pages_fetched: measured_count
oldest_week_received: source_week_key
newest_week_received: source_week_key
bucket_sum: measured_sum
current_count_observed: separately_fetched_count
reconciliation_difference: computed_difference
response_etags: retained_per_page
raw_payload_location: access_controlled_object_key
The snapshot lets an operator answer questions the latest response alone cannot:
- Did an old bucket change after a later fetch?
- Did a pagination failure remove early history?
- Was the current count observed before or after the history pages?
- Did a repository transfer change the lookup path?
- Did an API-version change alter the representation?
Do not describe any detected revision as “unstar churn” unless another documented signal establishes that cause. A changed bucket may reveal churn, correction, delayed processing, or another platform behaviour. The safe claim is that the aggregate observation changed.
Privacy-safe means some features must disappear
The June restriction was not a temporary outage to work around. GitHub said public stargazer and watcher lists had been misused to collect user data for spam. The new history endpoint is useful precisely because it does not restore those identities.
A compliant migration removes actor-dependent capabilities from the public-data path:
| Old feature | Privacy-safe replacement | Capability that should not be inferred |
|---|---|---|
| List recent stargazers | Show daily or weekly aggregate change | User names, profiles, employers, or locations |
| Enrich star events with account data | Segment by repository, release window, and source-owned campaign annotation | Demographic or firmographic composition of stargazers |
| Send outreach after a star | Publish an opt-in newsletter, discussion, issue template, or product signup | Consent to sales or marketing contact |
| Deduplicate named users across repositories | Compare aggregate series per repository | A cross-repository person graph |
| Attribute a star to one referrer | Annotate the aggregate timeline with launches and referrals measured elsewhere | Individual acquisition source |
Do not attempt to reverse the aggregation by combining tiny daily counts with public activity, social posts, follower lists, release reactions, or external profile databases. Even when a guess looks obvious, the endpoint did not supply that identity and the join recreates the misuse the replacement was designed to reduce.
For private repositories or other non-public resources, use a token with the documented read-level Metadata permission. Keep that access separate from write-capable automation; a charting job does not need repository contents or administration authority merely to read star history.
A launch annotation is not attribution
The endpoint will make launch charts easier to build. It will not make them causal.
Suppose a project publishes a major release on Wednesday and the relevant day slot rises. Several explanations can coexist:
- release notes drove new attention;
- a newsletter, conference talk, social post, or third-party article landed nearby;
- the repository appeared in Trending or another discovery surface;
- existing awareness converted later;
- GitHub's day boundary differs from the campaign's timezone;
- the current week is incomplete;
- the aggregate series was revised between observations.
Use a release window, not a release instant. Compare several closed weeks before and after the event, retain other known promotion dates, and report the result as an association:
Observed weekly star total increased after release X
not:
Release X acquired exactly N users
The first statement follows from an aggregate timeline. The second requires person-level acquisition evidence the endpoint intentionally does not contain.
For an open-source tool with a commercial product, use a metric ladder:
| Signal | What it can support |
|---|---|
| Star-history buckets | Repository attention over time |
| Package downloads or artifact pulls | Distribution activity, with ecosystem-specific caveats |
| Documentation visits and tagged referrals | Owned-site interest and source evidence |
| Install, activation, or successful first run | Product adoption |
| Opt-in account or newsletter signup | Permissioned relationship |
| Paid conversion, retained use, or qualified lead | Business outcome |
A star can be useful awareness evidence without being assigned the job of proving activation or revenue.
A migration checklist for star-tracking utilities
- Inventory actor-dependent features. Find profile enrichment, notifications, exports, outreach, deduplication, and identity joins—not only the API call itself.
- Classify each feature as replace, redesign, or remove. Aggregate charts can move to history buckets; actor-level features cannot claim an equivalent input.
- Pin the REST API version. Record
2026-03-10or the version actually requested with every observation. - Request 30 weeks per page. Follow
Linkheaders instead of constructing a guessed last page. - Store the raw bucket key. Keep GitHub's
weektimestamp and Sunday-first day array before applying timezone labels. - Upsert by repository ID and week. Do not use page number or owner/name as permanent identity.
- Validate every page set. Check seven day slots, daily sums, strict order, unique week keys, and complete pagination.
- Separate open and closed buckets. Exclude the current week from full-week comparisons or compare equal day coverage.
- Fetch current count independently. Reconcile it with the bucket sum and retain any difference as a diagnostic.
- Snapshot before overwrite. Keep observation time, API version, page coverage, ETags, totals, and a protected raw payload.
- Annotate rather than over-attribute. Join releases and campaigns at a window level, then use owned analytics for acquisition proof.
- Delete privacy-hostile fallbacks. Do not scrape the UI or reconstruct likely stargazers from other public traces.
- Reconnect attention to outcomes. Pair star trends with installs, successful use, support demand, signups, leads, or revenue appropriate to the product.
The acceptance rule is compact:
Every chart can state its grain, bucket status, page coverage, observation time, and supported claim—and no feature requires an identity the source deliberately withheld.
GitHub's new endpoint is a good privacy trade. Maintainers can recover useful historical shape, and public tools no longer need a list of people to draw a growth curve. The cost is real: some integrations must become less personal, less enrichable, and less certain about individual attribution.
That cost should stay visible in the architecture. Ingest the aggregate correctly, preserve its source boundaries, measure changes without pretending the buckets are immutable events, and move customer or contributor relationships onto explicit opt-in surfaces. The result is not a weaker version of a stargazer database. It is a better-defined analytics product that answers trend questions without turning attention into an unsolicited contact list.