[phase-1b] Add publish lifecycle + expanded read surfaces (v1.1.0)#2
Merged
Conversation
Implements the gem-side scope for Phase 1b of the instagram-publishing multi-repo project. Plan: linkmyphotos-rails docs/plans/instagram-publishing/phase-1b-gem-publish.md. New client modules: Publish (low-level create_media_container, publish_media_container, media_container_status plus high-level client.publish.single_image / carousel / reel / story helpers that run the full container-create -> poll-status -> publish lifecycle and return a PublishResult), Stories, Tagged, Insights, Comments, Hashtags. InstagramGraphAPI::Metrics ships the per-media-kind insight whitelist (MEDIA_INSIGHT_METRICS) and ACCOUNT_INSIGHT_METRICS, aligned with the Graph v21 schema. Deprecated names (impressions, engagement, video_views) are intentionally absent so 5b can't accidentally request them. InstagramGraphAPI::Validators ships Image, Video, Carousel surfacing IG publish constraints (JPEG, MP4/MOV, H.264/AAC, size, aspect ratio, duration, 2..10 carousel children) and raising ValidationError before container creation. Error#headers now carries response headers; TooManyRequests#retry_after parses the Retry-After header so callers can implement backoff. 101 specs pass against WebMock fixtures. Version bumped to 1.1.0; README and CHANGELOG updated.
There was a problem hiding this comment.
Pull request overview
This PR bumps the gem to v1.1.0 and expands the Instagram Graph API wrapper to support publishing (container lifecycle + helper facade), multiple new read surfaces (stories, tagged media, insights, comments/replies, hashtags), plus metric whitelists, validators, and improved rate-limit visibility via response headers.
Changes:
- Add Content Publishing API (
Client::Publish) with low-level endpoints and high-level helper workflow (create → poll → publish). - Add expanded read surfaces (
Stories,Tagged,Insights,Comments,Hashtags) and supporting fixtures/specs. - Add Metrics + Validators, and pass HTTP response headers through raised errors (including
TooManyRequests#retry_after).
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| CHANGELOG.md | Adds 1.1.0 release notes covering new publishing/read surfaces/validators/headers. |
| README.md | Documents v1.1.0 usage across all new modules, validators, and rate-limit handling. |
| lib/instagram_graph_api.rb | Requires new metrics + validator modules. |
| lib/instagram_graph_api/client.rb | Requires and includes new client modules (publish/read surfaces). |
| lib/instagram_graph_api/version.rb | Bumps version to 1.1.0. |
| lib/instagram_graph_api/error.rb | Adds #headers, TooManyRequests#retry_after, and ValidationError. |
| lib/instagram_graph_api/raise_http_exception.rb | Normalizes/passes response headers into raised exceptions. |
| lib/instagram_graph_api/metrics.rb | Introduces per-kind and account insight metric whitelists + helper. |
| lib/instagram_graph_api/client/publish.rb | Implements container create/poll/publish endpoints and helper facade. |
| lib/instagram_graph_api/client/stories.rb | Adds /stories read endpoint + alias. |
| lib/instagram_graph_api/client/tagged.rb | Adds /tags read endpoint + alias. |
| lib/instagram_graph_api/client/insights.rb | Adds media + account insights endpoints with metric serialization/whitelisting. |
| lib/instagram_graph_api/client/comments.rb | Adds comment listing + reply endpoints. |
| lib/instagram_graph_api/client/hashtags.rb | Adds hashtag search + top/recent media endpoints. |
| lib/instagram_graph_api/validators/image.rb | Adds image preflight validator (format/size/aspect). |
| lib/instagram_graph_api/validators/video.rb | Adds video preflight validator (format/codecs/size/duration). |
| lib/instagram_graph_api/validators/carousel.rb | Adds carousel preflight validator (count + per-child image validation). |
| spec/spec_helper.rb | Extends WebMock helpers to stub GET/POST with optional bodies/headers. |
| spec/instagram_graph_api_spec.rb | Updates version assertion to 1.1.0. |
| spec/instagram_graph_api/raise_http_exception_spec.rb | Verifies headers propagate and Retry-After parsing works. |
| spec/instagram_graph_api/metrics_spec.rb | Adds expectations around metric whitelist contents/deprecations. |
| spec/instagram_graph_api/client/publish_spec.rb | Tests publish endpoints + helper lifecycle (polling/error/timeout). |
| spec/instagram_graph_api/client/stories_spec.rb | Tests stories endpoint + alias + error raising. |
| spec/instagram_graph_api/client/tagged_spec.rb | Tests tagged endpoint + alias + error raising. |
| spec/instagram_graph_api/client/insights_spec.rb | Tests metric serialization shapes + user/media insights calls. |
| spec/instagram_graph_api/client/comments_spec.rb | Tests comment reads + replies + error raising. |
| spec/instagram_graph_api/client/hashtags_spec.rb | Tests hashtag search + top/recent media reads. |
| spec/instagram_graph_api/validators/image_spec.rb | Tests image validator success/failure modes. |
| spec/instagram_graph_api/validators/video_spec.rb | Tests video validator success/failure modes. |
| spec/instagram_graph_api/validators/carousel_spec.rb | Tests carousel validator child count + invalid child indexing. |
| spec/fixtures/errors/429_with_retry_after.json | Adds 429 error payload fixture for Retry-After behavior. |
| spec/fixtures/publish/* | Adds fixtures for container create/status/publish flows. |
| spec/fixtures/stories.json | Adds stories endpoint fixture. |
| spec/fixtures/tagged.json | Adds tagged media fixture. |
| spec/fixtures/insights/* | Adds media/account insights fixtures. |
| spec/fixtures/comments/* | Adds comments and replies fixtures. |
| spec/fixtures/hashtags/* | Adds hashtag search/top media fixtures. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…cs doc Six unique findings from the Copilot review on #2 (each duplicated twice in the review threads): - Metrics: docstring claimed `plays` on non-reel media is deprecated, but `:video` correctly includes it (still supported on feed videos in Graph v21+). Updated the comment to match the data, and added a spec pinning `plays` to `:video`/`:reel` and asserting absence from `:image`/`:story`/`:carousel` so future edits can't drift the invariant. - Error: `TooManyRequests#retry_after` docstring promised an `x-business-use-case-usage` fallback that the implementation never parsed. Dropped the claim; behavior is unchanged (standard Retry-After only, which is what the spec already covers). - Validators::Image / Validators::Video: `size_bytes`, `aspect_ratio`, `duration_seconds` were compared numerically and blew up with TypeError when callers (e.g. Rails controllers re-hydrating JSON params) passed string metadata. Routed each numeric kwarg through Integer()/Float() coercion that adds a collected validation error on failure instead of raising. New specs cover the string-coerces-cleanly path and the string-is-un-coercible path. - Publish spec: the "drops nil params" example matched any POST body, so the test passed even if caption was being sent. Replaced the stub with a block matcher that fails the request unless the encoded body has no `caption` key. Full suite: 106 examples, 0 failures.
…oaden retry_after rescue - media_container_status yard listed PUBLISHED as a possible status_code, but the Graph API only returns PUBLISHED on already- published containers — not during the IN_PROGRESS → FINISHED → publish flow this method is documented for. Removed PUBLISHED from the documented set so the docstring matches what the helper actually observes. - TooManyRequests#retry_after rescued only ArgumentError, but Integer(non_string_truthy) raises TypeError (e.g. if a Faraday adapter hands back an Array header). Added TypeError to the rescue so the method honors its "nil when missing or non-numeric" contract instead of masking the original 429. Spec covers the case. 107 examples, 0 failures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase plan:
linkmyphotos-rails docs/plans/instagram-publishing/phase-1b-gem-publish.mdMaster pointer:
linkmyphotos-rails docs/plans/instagram-publishing/README.md— bumped in the companion PR sixoverground/linkmyphotos-rails#claude/phase-1b-gem-publishSummary
Expands the gem to cover every Instagram Graph API surface the project
needs, lining up the building blocks the Rails ingestion layer will
consume in phase 5b and the publishing engine already merged in phase 3.
Version bumped to
1.1.0.Content Publishing (
Client::Publish)Two layers:
create_media_container(POST /{ig-user-id}/media),publish_media_container(POST /{ig-user-id}/media_publish),media_container_status(GET /{ig-container-id}?fields=status_code,status).client.publish.single_image,.carousel,.reel,.story— each runs the full container-create → poll-status →publish dance and returns a
PublishResultstruct exposingcontainer_id,media_id, andstatus(:published,:error,:timeout). Polling is configurable (poll:,poll_interval:,poll_timeout:); the default 5-minute timeout returns:timeoutinstead of hanging the Rails worker forever.Expanded read surfaces
Client::Stories#user_stories—GET /{ig-user-id}/stories(24-hour window).Client::Tagged#user_tagged_media—GET /{ig-user-id}/tags.Client::Insights#media_insights—GET /{ig-media-id}/insights.Client::Insights#user_insights—GET /{ig-user-id}/insights. Acceptsa metric String, Array, media-kind Symbol, or the
:accountsentinel(each Symbol expands to the per-kind whitelist).
Client::Comments#media_comments,#reply_to_media,#reply_to_comment,#comment_replies.Client::Hashtags#hashtag_search,#hashtag_top_media,#hashtag_recent_media.InstagramGraphAPI::MetricsInsight metric whitelist consumed by phase 5b's Rails ingestion layer:
MEDIA_INSIGHT_METRICS[:image|:video|:reel|:story|:carousel]—per-media-kind metric lists matching the Graph v21 schema. Reels
include
views,ig_reels_avg_watch_time,ig_reels_video_view_total_time; stories includereach,replies,exits,views.ACCOUNT_INSIGHT_METRICS— account-level v1 must-haves(
views,profile_views,follower_count,accounts_engaged,total_interactions,reach,likes,comments,shares,saves).impressions,engagement,video_views,playson non-reel media) are intentionally absent — a spec assertsthis so we can't accidentally request them and trip the schema.
InstagramGraphAPI::ValidatorsValidators::Image.validate(url, size_bytes:, aspect_ratio:, format:)—JPEG, ≤ 8 MB, aspect 0.8..1.91.
Validators::Video.validate(url, kind: :feed|:reel|:story, size_bytes:, duration_seconds:, format:, video_codec:, audio_codec:)— MP4/MOV,H.264/AAC, ≤ 100 MB, kind-specific duration caps (feed 60s, reel 90s,
story 60s).
Validators::Carousel.validate(child_urls)— 2..10 children, each oneitself a valid image.
Raises
InstagramGraphAPI::ValidationErrorwith#errorslisting everyissue found, so callers can short-circuit before burning a container
quota. The gem does not fetch remote URLs — callers pass whatever
metadata they already have; the validator checks what it was given.
Rate-limit visibility
Error#headersnow carries the response headers.TooManyRequests#retry_afterparses theRetry-Afterheader socallers can implement backoff.
Files touched
New:
lib/instagram_graph_api/client/publish.rblib/instagram_graph_api/client/stories.rblib/instagram_graph_api/client/tagged.rblib/instagram_graph_api/client/insights.rblib/instagram_graph_api/client/comments.rblib/instagram_graph_api/client/hashtags.rblib/instagram_graph_api/validators/{image,video,carousel}.rblib/instagram_graph_api/metrics.rbspec/instagram_graph_api/client/{publish,stories,tagged,insights,comments,hashtags}_spec.rbspec/instagram_graph_api/validators/{image,video,carousel}_spec.rbspec/instagram_graph_api/metrics_spec.rbspec/instagram_graph_api/raise_http_exception_spec.rbspec/fixtures/{publish,insights,comments,hashtags,stories,tagged}*andspec/fixtures/errors/429_with_retry_after.jsonModified:
lib/instagram_graph_api.rb— require new fileslib/instagram_graph_api/client.rb— include new moduleslib/instagram_graph_api/error.rb— add#headers,TooManyRequests#retry_after,ValidationErrorlib/instagram_graph_api/raise_http_exception.rb— pass headers throughlib/instagram_graph_api/version.rb—1.0.0→1.1.0spec/spec_helper.rb— addstub_graph_post, tighterstub_graph_getspec/instagram_graph_api_spec.rb— version assertion bumpedREADME.md— usage docs for every new moduleCHANGELOG.md— 1.1.0 entryTesting notes
Phase plan's test command:
bundle exec rspec.Locally on the ephemeral exec container (
ruby 3.3.6):Gem builds cleanly:
The repo's own CI (
.github/workflows/ci.yml) runsbundle exec rspecon Ruby 3.1, 3.2, 3.3 and will exercise the full suite on this branch.
Review checklist (from the plan)
MEDIA_INSIGHT_METRICSconstants match the current Graph v21 schemaimpressions,engagement,video_views,playson non-reel media) excluded — asserted inmetrics_spec.rbstatus: :timeout, default 5-minute deadline)Out of scope (deferred)
v1.1.0git tag — created out-of-band by the maintainer after this PR merges; the master plan's acceptance criteria treats the tag as a release step, not a code change.linkmyphotos-rails'sGemfilepin fromv1.0.0→v1.1.0— phase 5b is the first Rails phase that actually calls the new modules; bumping now would add dead code.Phase pointer
The master plan README in
linkmyphotos-railsis being bumped in thecompanion PR on
claude/phase-1b-gem-publishover in that repo(marks 5a merged, marks 1b in progress).
After this and the README pointer PR merge, Phase 5b — Rails insights
& expanded ingestion is unlocked (it depends on both 1b and 5a, and
5a is already in).
Generated by Claude Code
Generated by Claude Code