Add User.contributionCalendar GraphQL field
What does this MR do and why?
Adds a contributionCalendar field to the GraphQL User interface, exposing the data behind the profile page's contribution heatmap to API clients.
Today the calendar is only reachable through the session-only web route /users/:username/calendar.json (UsersController#calendar), where a bearer token is never read. An API client such as GitLab Mobile can therefore neither render the heatmap nor learn the profile owner's UTC offset, which the Web UI gets from UsersHelper#user_activity_calendar_data and needs to place the days correctly.
The field exposes what the controller and helper already compute — Gitlab::ContributionsCalendar#activity_dates (the past year, counted on the contributor's calendar days) plus the owner's timezone and its current UTC offset — through GraphQL, gated the same way: it resolves to null unless the current user can :read_user_profile the target (private profiles, blocked and unconfirmed users), the gate behind the controller's authorize_read_user_profile!. No new queries, no new permissions; the calendar's own project visibility rules (ContributedProjectsFinder, include_private_contributions) apply unchanged.
Schema
interface User {
"""
Contribution calendar of the user: the profile page activity heatmap for the past year.
Null when the current user cannot read the profile.
"""
contributionCalendar: UserContributionCalendar # Experiment, 19.5
}
type UserContributionCalendar {
utcOffset: Int! # offset of the user's timezone from UTC, in seconds, as of the request
timezone: String! # identifier of the user's timezone, e.g. "Asia/Tokyo"
days: [UserContributionDay!]! # days with at least one contribution, oldest first
totalCount: Int! # total contributions in the past year
}
type UserContributionDay {
date: Date! # in the user's timezone
count: Int!
}UserContributionCalendar and UserContributionDay are value objects authorized by the parent field (Graphql/AuthorizeTypes disabled, authorize_granular_token skip_reason: :parent_authorizes). days and totalCount share one memoized activity_dates query.
Files
app/graphql/resolvers/users/contribution_calendar_resolver.rb— theread_user_profilegate, returns aGitlab::ContributionsCalendarapp/graphql/types/users/contribution_calendar_type.rb,app/graphql/types/users/contribution_day_type.rb— the two value typesapp/graphql/types/user_interface.rb— the field, besideevents- Generated:
doc/api/graphql/reference/_index.md,public/-/graphql/introspection_result.json(theno_deprecatedschema is unchanged, as experiments are excluded from it)
References
Related to GitLab Mobile (the iOS client's profile page renders the heatmap from this field).
How to set up and validate locally
-
Pick a user with recent activity and run, with a PAT (
apiorread_apiscope) or as the signed-in user:query { user(username: "root") { contributionCalendar { utcOffset timezone totalCount days { date count } } } } -
Compare
dayswithhttp://gdk.test:3000/users/root/calendar.json(same dates and counts) andutcOffsetwith thedata-utc-offsetthe profile page's activity calendar is mounted with. -
Set the user's profile to private (Edit profile > Private profile) and query again as another user:
contributionCalendarisnull. As the user themselves it is still returned.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.
- Specs:
spec/graphql/types/users/contribution_calendar_type_spec.rb,spec/graphql/types/users/contribution_day_type_spec.rb,spec/requests/api/graphql/user/contribution_calendar_query_spec.rb(timezone-day boundary, private profile for another viewer / the owner) - GraphQL docs and introspection schemas regenerated
- No new database queries; the resolver reuses
Gitlab::ContributionsCalendar