<- blog

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#analytics#developer-tools#privacy

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_page has a maximum of 30 for this endpoint, not the 100 common to many GitHub routes;
  • page has 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:

  1. Closed-week reporting: omit the newest bucket until the next bucket appears. This is the safest default for weekly trend and release reporting.
  2. Week-to-date reporting: compare only the same number of day slots from the prior week and label the comparison as partial.
  3. 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

  1. Inventory actor-dependent features. Find profile enrichment, notifications, exports, outreach, deduplication, and identity joins—not only the API call itself.
  2. Classify each feature as replace, redesign, or remove. Aggregate charts can move to history buckets; actor-level features cannot claim an equivalent input.
  3. Pin the REST API version. Record 2026-03-10 or the version actually requested with every observation.
  4. Request 30 weeks per page. Follow Link headers instead of constructing a guessed last page.
  5. Store the raw bucket key. Keep GitHub's week timestamp and Sunday-first day array before applying timezone labels.
  6. Upsert by repository ID and week. Do not use page number or owner/name as permanent identity.
  7. Validate every page set. Check seven day slots, daily sums, strict order, unique week keys, and complete pagination.
  8. Separate open and closed buckets. Exclude the current week from full-week comparisons or compare equal day coverage.
  9. Fetch current count independently. Reconcile it with the bucket sum and retain any difference as a diagnostic.
  10. Snapshot before overwrite. Keep observation time, API version, page coverage, ETags, totals, and a protected raw payload.
  11. Annotate rather than over-attribute. Join releases and campaigns at a window level, then use owned analytics for acquisition proof.
  12. Delete privacy-hostile fallbacks. Do not scrape the UI or reconstruct likely stargazers from other public traces.
  13. 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.

Need technical help?

I'm a software engineer who builds web apps, APIs, and AI tooling. If you've got a project or a problem to talk through, book a free 30-minute call.

Book time with me ->