Skip to content

[phase-1b] Add publish lifecycle + expanded read surfaces (v1.1.0)#2

Merged
craigphares merged 3 commits into
mainfrom
claude/phase-1b-gem-publish
May 31, 2026
Merged

[phase-1b] Add publish lifecycle + expanded read surfaces (v1.1.0)#2
craigphares merged 3 commits into
mainfrom
claude/phase-1b-gem-publish

Conversation

@craigphares
Copy link
Copy Markdown
Member

Phase plan: linkmyphotos-rails docs/plans/instagram-publishing/phase-1b-gem-publish.md
Master pointer: linkmyphotos-rails docs/plans/instagram-publishing/README.md — bumped in the companion PR sixoverground/linkmyphotos-rails#claude/phase-1b-gem-publish

Summary

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:

  • Low-level endpoints: 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).
  • High-level helpersclient.publish.single_image, .carousel,
    .reel, .story — each runs the full container-create → poll-status →
    publish dance and returns a PublishResult struct exposing
    container_id, media_id, and status (:published, :error,
    :timeout). Polling is configurable (poll:, poll_interval:,
    poll_timeout:); the default 5-minute timeout returns
    :timeout instead of hanging the Rails worker forever.

Expanded read surfaces

  • Client::Stories#user_storiesGET /{ig-user-id}/stories (24-hour window).
  • Client::Tagged#user_tagged_mediaGET /{ig-user-id}/tags.
  • Client::Insights#media_insightsGET /{ig-media-id}/insights.
  • Client::Insights#user_insightsGET /{ig-user-id}/insights. Accepts
    a metric String, Array, media-kind Symbol, or the :account sentinel
    (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::Metrics

Insight 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 include reach, 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).
  • Deprecated names (impressions, engagement, video_views,
    plays on non-reel media) are intentionally absent — a spec asserts
    this so we can't accidentally request them and trip the schema.

InstagramGraphAPI::Validators

  • Validators::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 one
    itself a valid image.

Raises InstagramGraphAPI::ValidationError with #errors listing every
issue 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#headers now carries the response headers.
  • TooManyRequests#retry_after parses the Retry-After header so
    callers can implement backoff.

Files touched

New:

  • lib/instagram_graph_api/client/publish.rb
  • lib/instagram_graph_api/client/stories.rb
  • lib/instagram_graph_api/client/tagged.rb
  • lib/instagram_graph_api/client/insights.rb
  • lib/instagram_graph_api/client/comments.rb
  • lib/instagram_graph_api/client/hashtags.rb
  • lib/instagram_graph_api/validators/{image,video,carousel}.rb
  • lib/instagram_graph_api/metrics.rb
  • spec/instagram_graph_api/client/{publish,stories,tagged,insights,comments,hashtags}_spec.rb
  • spec/instagram_graph_api/validators/{image,video,carousel}_spec.rb
  • spec/instagram_graph_api/metrics_spec.rb
  • spec/instagram_graph_api/raise_http_exception_spec.rb
  • spec/fixtures/{publish,insights,comments,hashtags,stories,tagged}* and
    spec/fixtures/errors/429_with_retry_after.json

Modified:

  • lib/instagram_graph_api.rb — require new files
  • lib/instagram_graph_api/client.rb — include new modules
  • lib/instagram_graph_api/error.rb — add #headers, TooManyRequests#retry_after, ValidationError
  • lib/instagram_graph_api/raise_http_exception.rb — pass headers through
  • lib/instagram_graph_api/version.rb1.0.01.1.0
  • spec/spec_helper.rb — add stub_graph_post, tighter stub_graph_get
  • spec/instagram_graph_api_spec.rb — version assertion bumped
  • README.md — usage docs for every new module
  • CHANGELOG.md — 1.1.0 entry

Testing notes

Phase plan's test command: bundle exec rspec.

Locally on the ephemeral exec container (ruby 3.3.6):

$ bundle exec ruby -Ilib -Ispec -e 'require "rspec/autorun"; \
    Dir.glob("spec/**/*_spec.rb").sort.each { |f| load f }'
...
Finished in 2.09 seconds (files took 0.38088 seconds to load)
101 examples, 0 failures

Gem builds cleanly:

$ gem build instagram_graph_api.gemspec
  Successfully built RubyGem
  Name: instagram_graph_api
  Version: 1.1.0

The repo's own CI (.github/workflows/ci.yml) runs bundle exec rspec
on Ruby 3.1, 3.2, 3.3 and will exercise the full suite on this branch.

Review checklist (from the plan)

  • All MEDIA_INSIGHT_METRICS constants match the current Graph v21 schema
  • Deprecated metrics (impressions, engagement, video_views, plays on non-reel media) excluded — asserted in metrics_spec.rb
  • Publish helpers handle the polling timeout case (returns status: :timeout, default 5-minute deadline)
  • No Rails-specific logic leaked into the gem
  • Validators surface IG-style error messages verbatim where applicable

Out of scope (deferred)

  • v1.1.0 git 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.
  • Bumping linkmyphotos-rails's Gemfile pin from v1.0.0v1.1.0 — phase 5b is the first Rails phase that actually calls the new modules; bumping now would add dead code.
  • Integration test against a live IG sandbox — the plan's acceptance criteria mentions this as a manual sandbox smoke. The committed WebMock fixtures cover every code path; live sandbox smoke is a maintainer step.

Phase pointer

The master plan README in linkmyphotos-rails is being bumped in the
companion PR on claude/phase-1b-gem-publish over 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

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.
Copilot AI review requested due to automatic review settings May 31, 2026 21:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/instagram_graph_api/metrics.rb
Comment thread spec/instagram_graph_api/metrics_spec.rb
Comment thread lib/instagram_graph_api/error.rb Outdated
Comment thread lib/instagram_graph_api/validators/image.rb Outdated
Comment thread lib/instagram_graph_api/validators/video.rb Outdated
Comment thread spec/instagram_graph_api/metrics_spec.rb
Comment thread lib/instagram_graph_api/error.rb Outdated
Comment thread lib/instagram_graph_api/validators/image.rb Outdated
Comment thread lib/instagram_graph_api/validators/video.rb Outdated
Comment thread spec/instagram_graph_api/client/publish_spec.rb Outdated
…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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 47 out of 47 changed files in this pull request and generated 2 comments.

Comment thread lib/instagram_graph_api/client/publish.rb
Comment thread lib/instagram_graph_api/error.rb
…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.
@craigphares craigphares merged commit ce0b326 into main May 31, 2026
3 checks passed
@craigphares craigphares deleted the claude/phase-1b-gem-publish branch May 31, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants