feat: fold Google Analytics into the SEO report (lib/ga.sh) #329

Closed
opened 2026-07-03 04:53:15 +00:00 by igor · 3 comments
Collaborator

Goal (re-scoped for the agent's turn budget — see Execution notes)

GA client + SEO report fold-in. Follow-on to #309 (GA access — done: the service account
reads GSC + 7 GA properties). The CEO digest fold-in has been split into a separate follow-up
(it depends on lib/ga.sh from this ticket), so each PR completes inside the agent's 50-turn
budget — this ticket ran out of turns twice trying to do both at once.

Principle: "GSC and/or GA — whatever's available." GA is additive and OPTIONAL: a domain with
no GA property must render exactly as it does today.

Part A — lib/ga.sh (new; mirror lib/gsc.sh)

Pure-ish, scripted (no LLM), authing through the existing lib/google-auth.sh:

  • ga_access_token — wrap google_sa_access_token "https://www.googleapis.com/auth/analytics.readonly".
    Empty + rc=1 on failure.
  • ga_property_for_domain <domain> — resolve the numeric property id dynamically: GET
    https://analyticsadmin.googleapis.com/v1beta/accountSummaries?pageSize=200, return the
    propertySummaries[].property (strip properties/) whose displayName == <domain>. Empty if
    none. No static map — mirrors GSC's zero-config enumeration.
  • ga_run_report <property_id> <start> <end> <dimensions_csv> <metrics_csv> — POST
    https://analyticsdata.googleapis.com/v1beta/properties/<id>:runReport; echo raw JSON; match
    gsc_query's error posture (empty-rows JSON + rc on failure).

Reference — properties the SA can read (do NOT hardcode; sanity-check only):
certifiedtradejobs.com 535124688, vpsshowdown.com 487911904, sharktank.co 404967917, scenekids.com 535254606, thatgirljen.com 371484954, debtmom.com 526352480, holidayapi.com 387040916.

Part B — SEO report fold-in (lib/seo-analysis.sh, bin/tick.sh do_seo_tick)

When the analyzed domain resolves to a GA property, fetch GA for the same 28-day window
(seo_window) and add an "On-site behavior (GA)" section to the report: sessions,
engagedSessions, engagementRate, totalUsers, keyEvents if present. Thread through
seo_build_report and BOTH renderers (seo_render_markdown, seo_render_html). Optionality is
mandatory:
no GA property → renders exactly as today (GSC-only). No new env knob.

Tests — REQUIRED (top code-review finding)

  • New bin/test-ga.sh against fixture JSON: ga_property_for_domain match/no-match,
    ga_run_report parsing. Skip-safe (exit 0 + notice if jq/python3 absent), like
    bin/test-ceo.sh.
  • Cover both SEO GA branches (with-property renders the section; without renders unchanged). Keep
    it minimal — extend an existing test rather than a large new suite if that's cheaper.
  • bash bin/check-sync.sh green.

Execution notes (turn budget — this ticket failed twice on these)

  • You have a 50-turn budget, and the harness commits your work ONLY when you exit cleanly
    ("done"). Both prior attempts wrote everything correctly but ran out of turns re-running the
    test suite, so nothing committed. Build, verify once, then STOP. Don't re-run the full
    suite repeatedly or re-explore.
  • Run scripts as bash bin/test-ga.sh / bash bin/check-sync.shno > redirects, no
    &&/; compound commands, no ./relative execution.
    Those forms trip the permission
    profile, get denied, and waste turns.

Constraints

  • set -euo pipefail; 2-space indent; check-then-act; log for logging.
  • No new env knobs. Do not touch agent-settings.json, systemd/, .forgejo/workflows/.

Acceptance criteria

  • lib/ga.sh with the three functions; dynamic property resolution (no static map).
  • GA section in the SEO report (both renderers); optional, degrades to GSC-only.
  • bin/test-ga.sh + SEO branch coverage; bash bin/check-sync.sh green.
  • No new env vars; off-limits files untouched.
<!-- agent:enqueue --> ## Goal (re-scoped for the agent's turn budget — see Execution notes) GA client + **SEO report** fold-in. Follow-on to #309 (GA access — done: the service account reads GSC + 7 GA properties). The **CEO digest** fold-in has been split into a separate follow-up (it depends on `lib/ga.sh` from this ticket), so each PR completes inside the agent's 50-turn budget — this ticket ran out of turns twice trying to do both at once. Principle: **"GSC and/or GA — whatever's available."** GA is additive and OPTIONAL: a domain with no GA property must render exactly as it does today. ## Part A — `lib/ga.sh` (new; mirror `lib/gsc.sh`) Pure-ish, scripted (no LLM), authing through the existing `lib/google-auth.sh`: - `ga_access_token` — wrap `google_sa_access_token "https://www.googleapis.com/auth/analytics.readonly"`. Empty + rc=1 on failure. - `ga_property_for_domain <domain>` — resolve the numeric property id **dynamically**: GET `https://analyticsadmin.googleapis.com/v1beta/accountSummaries?pageSize=200`, return the `propertySummaries[].property` (strip `properties/`) whose `displayName == <domain>`. Empty if none. **No static map** — mirrors GSC's zero-config enumeration. - `ga_run_report <property_id> <start> <end> <dimensions_csv> <metrics_csv>` — POST `https://analyticsdata.googleapis.com/v1beta/properties/<id>:runReport`; echo raw JSON; match `gsc_query`'s error posture (empty-rows JSON + rc on failure). Reference — properties the SA can read (do NOT hardcode; sanity-check only): `certifiedtradejobs.com 535124688, vpsshowdown.com 487911904, sharktank.co 404967917, scenekids.com 535254606, thatgirljen.com 371484954, debtmom.com 526352480, holidayapi.com 387040916`. ## Part B — SEO report fold-in (`lib/seo-analysis.sh`, `bin/tick.sh` `do_seo_tick`) When the analyzed domain resolves to a GA property, fetch GA for the **same 28-day window** (`seo_window`) and add an "On-site behavior (GA)" section to the report: `sessions`, `engagedSessions`, `engagementRate`, `totalUsers`, `keyEvents` if present. Thread through `seo_build_report` and BOTH renderers (`seo_render_markdown`, `seo_render_html`). **Optionality is mandatory:** no GA property → renders exactly as today (GSC-only). No new env knob. ## Tests — REQUIRED (top code-review finding) - New `bin/test-ga.sh` against **fixture JSON**: `ga_property_for_domain` match/no-match, `ga_run_report` parsing. Skip-safe (exit 0 + notice if `jq`/`python3` absent), like `bin/test-ceo.sh`. - Cover both SEO GA branches (with-property renders the section; without renders unchanged). Keep it minimal — extend an existing test rather than a large new suite if that's cheaper. - `bash bin/check-sync.sh` green. ## Execution notes (turn budget — this ticket failed twice on these) - You have a **50-turn budget**, and the harness commits your work ONLY when you exit cleanly ("done"). Both prior attempts wrote everything correctly but ran out of turns re-running the test suite, so nothing committed. **Build, verify once, then STOP.** Don't re-run the full suite repeatedly or re-explore. - Run scripts as `bash bin/test-ga.sh` / `bash bin/check-sync.sh` — **no `>` redirects, no `&&`/`;` compound commands, no `./relative` execution.** Those forms trip the permission profile, get denied, and waste turns. ## Constraints - `set -euo pipefail`; 2-space indent; check-then-act; `log` for logging. - **No new env knobs.** Do **not** touch `agent-settings.json`, `systemd/`, `.forgejo/workflows/`. ## Acceptance criteria - [ ] `lib/ga.sh` with the three functions; dynamic property resolution (no static map). - [ ] GA section in the SEO report (both renderers); optional, degrades to GSC-only. - [ ] `bin/test-ga.sh` + SEO branch coverage; `bash bin/check-sync.sh` green. - [ ] No new env vars; off-limits files untouched.
igor self-assigned this 2026-07-03 04:54:30 +00:00
igor removed their assignment 2026-07-03 05:03:08 +00:00
Author
Collaborator

The agent completed with no work produced and no blocker reported. Investigate. (claude exit: 1, elapsed 515s)

last bytes of claude output
I'll start by exploring the existing GSC implementation that this GA work needs to mirror.
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool: Bash]
[tool_result]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Read]
[tool_result]
[tool: Bash]
[tool_result]
Now let's find where `ceo_read_gsc` is called in the digest assembly, and view the full `do_seo_tick`.
[tool: Bash]
[tool_result]
[tool: Read]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Read]
[tool_result]
Let me look at line 2080-2100 to see the digest assembly context, and check `ceo_metrics_store`/`ceo_read_metrics` too since they seem to be a sibling pattern.
[tool: Read]
[tool_result]
[tool: Read]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Read]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
Now let's check `docs/architecture.md` for GSC/CEO documentation that might also need touching, and view the rest of `lib/ceo.sh` around `ceo_build_prompt` to see how the activity text gets used/labeled (to make sure GA section headers read well in the digest).
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Read]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Read]
[tool_result]
[tool: Read]
[tool_result]
Now I have a clear picture. Let me check one more thing — how `ceo_metrics_store`/`ceo_read_metrics` pattern works, since it's a close sibling to what I'm adding, and check `AGENT_CONFIG_FILE` default usage consistency.
[tool: Bash]
[tool_result]
[tool: Read]
[tool_result]
Good, this confirms the pattern. Now let's write `lib/ga.sh`.
[tool: Write]
[tool_result]
Now let's source it in `bin/tick.sh` right after `lib/gsc.sh`.
[tool: Read]
[tool_result]
[tool: Edit]
[tool_result]
Now let's update `seo_build_report` in `lib/seo-analysis.sh` to accept optional GA totals.
[tool: Read]
[tool_result]
[tool: Read]
[tool_result]
Now let's edit `seo_build_report` to thread through optional GA totals (11th and 12th positional args).
[tool: Edit]
[tool_result]
[tool: Edit]
[tool_result]
Now let's add the GA section to both renderers.
[tool: Read]
[tool_result]
[tool: Edit]
[tool_result]
Now let's do the same for `seo_render_html`.
[tool: Read]
[tool_result]
[tool: Edit]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
No tests depend on that string. I'll leave the footer wording as-is to keep the diff focused — the GA section is clearly labeled on its own. Now let's update `do_seo_tick` in `bin/tick.sh` to fetch and pass GA data.
[tool: Read]
[tool_result]
[tool: Edit]
[tool_result]
Now let's add `ceo_read_ga` to `lib/ceo.sh`, right after `ceo_read_gsc`.
[tool: Read]
[tool_result]
[tool: Edit]
[tool_result]
Now wire it into the digest assembly in `bin/tick.sh`:
[tool: Edit]
[tool_result]
[tool: Bash]
[tool_result]
Let me view the full `ceo_read_gsc` test block for exact style, then extend it with a `ceo_read_ga` section right after.
[tool: Read]
[tool_result]
[tool: Read]
[tool_result]
[tool: Edit]
[tool_result]
[tool: Edit]
[tool_result]
Now let's write `bin/test-ga.sh`.
[tool: Write]
[tool_result]
[tool: Bash]
[tool_result]
Good, that confirms empty input degrades to empty output with rc 0, which is what the test expects. Let's make `bin/test-ga.sh` executable and run all three test files.
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
[tool: Bash]
[tool_result]
The agent completed with no work produced and no blocker reported. Investigate. (claude exit: 1, elapsed 515s) <details><summary>last bytes of claude output</summary> ``` I'll start by exploring the existing GSC implementation that this GA work needs to mirror. [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool: Bash] [tool_result] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Read] [tool_result] [tool: Bash] [tool_result] Now let's find where `ceo_read_gsc` is called in the digest assembly, and view the full `do_seo_tick`. [tool: Bash] [tool_result] [tool: Read] [tool_result] [tool: Bash] [tool_result] [tool: Read] [tool_result] Let me look at line 2080-2100 to see the digest assembly context, and check `ceo_metrics_store`/`ceo_read_metrics` too since they seem to be a sibling pattern. [tool: Read] [tool_result] [tool: Read] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Read] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] Now let's check `docs/architecture.md` for GSC/CEO documentation that might also need touching, and view the rest of `lib/ceo.sh` around `ceo_build_prompt` to see how the activity text gets used/labeled (to make sure GA section headers read well in the digest). [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Read] [tool_result] [tool: Bash] [tool_result] [tool: Read] [tool_result] [tool: Read] [tool_result] Now I have a clear picture. Let me check one more thing — how `ceo_metrics_store`/`ceo_read_metrics` pattern works, since it's a close sibling to what I'm adding, and check `AGENT_CONFIG_FILE` default usage consistency. [tool: Bash] [tool_result] [tool: Read] [tool_result] Good, this confirms the pattern. Now let's write `lib/ga.sh`. [tool: Write] [tool_result] Now let's source it in `bin/tick.sh` right after `lib/gsc.sh`. [tool: Read] [tool_result] [tool: Edit] [tool_result] Now let's update `seo_build_report` in `lib/seo-analysis.sh` to accept optional GA totals. [tool: Read] [tool_result] [tool: Read] [tool_result] Now let's edit `seo_build_report` to thread through optional GA totals (11th and 12th positional args). [tool: Edit] [tool_result] [tool: Edit] [tool_result] Now let's add the GA section to both renderers. [tool: Read] [tool_result] [tool: Edit] [tool_result] Now let's do the same for `seo_render_html`. [tool: Read] [tool_result] [tool: Edit] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] No tests depend on that string. I'll leave the footer wording as-is to keep the diff focused — the GA section is clearly labeled on its own. Now let's update `do_seo_tick` in `bin/tick.sh` to fetch and pass GA data. [tool: Read] [tool_result] [tool: Edit] [tool_result] Now let's add `ceo_read_ga` to `lib/ceo.sh`, right after `ceo_read_gsc`. [tool: Read] [tool_result] [tool: Edit] [tool_result] Now wire it into the digest assembly in `bin/tick.sh`: [tool: Edit] [tool_result] [tool: Bash] [tool_result] Let me view the full `ceo_read_gsc` test block for exact style, then extend it with a `ceo_read_ga` section right after. [tool: Read] [tool_result] [tool: Read] [tool_result] [tool: Edit] [tool_result] [tool: Edit] [tool_result] Now let's write `bin/test-ga.sh`. [tool: Write] [tool_result] [tool: Bash] [tool_result] Good, that confirms empty input degrades to empty output with rc 0, which is what the test expects. Let's make `bin/test-ga.sh` executable and run all three test files. [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] [tool: Bash] [tool_result] ``` </details>
igor self-assigned this 2026-07-03 05:17:44 +00:00
Author
Collaborator

The agent produced no work on this issue twice. The issue is probably unclear, requires context Claude can't reach, or has a setup problem. Investigate, then remove Status/Blocked to re-queue.

The agent produced no work on this issue twice. The issue is probably unclear, requires context Claude can't reach, or has a setup problem. Investigate, then remove `Status/Blocked` to re-queue.
igor removed their assignment 2026-07-03 05:26:29 +00:00
igor changed title from feat: fold Google Analytics into the SEO report and CEO digest (lib/ga.sh) to feat: fold Google Analytics into the SEO report (lib/ga.sh) 2026-07-03 05:41:02 +00:00
Author
Collaborator

Re-scoped + re-queued (CoS Igor). This ran out of the agent's 50-turn budget twice — the build was complete both times (lib/ga.sh + SEO + CEO + tests, all passing) but claude hit turn 50 before the harness' exit-0 commit could fire, so nothing landed. The root cause is the hardcoded --max-turns 50 being too low for a ticket this size (a real fix — making it configurable/higher for issue work — is a separate harness change for Josh to merge).

To land a PR within the existing cap I split the work: this ticket is now SEO report + lib/ga.sh only; the CEO digest fold-in moved to a follow-up (it depends on lib/ga.sh here). Removed Status/Blocked and cleared the assignee to re-queue. Execution notes added to the body (commit-and-stop; avoid the permission-denied command forms that also burned turns).

**Re-scoped + re-queued (CoS Igor).** This ran out of the agent's 50-turn budget twice — the build was complete both times (lib/ga.sh + SEO + CEO + tests, all passing) but claude hit turn 50 before the harness' exit-0 commit could fire, so nothing landed. The root cause is the hardcoded `--max-turns 50` being too low for a ticket this size (a real fix — making it configurable/higher for issue work — is a separate harness change for Josh to merge). To land a PR within the existing cap I split the work: this ticket is now **SEO report + `lib/ga.sh`** only; the **CEO digest** fold-in moved to a follow-up (it depends on `lib/ga.sh` here). Removed `Status/Blocked` and cleared the assignee to re-queue. Execution notes added to the body (commit-and-stop; avoid the permission-denied command forms that also burned turns).
igor self-assigned this 2026-07-03 05:41:33 +00:00
igor added spent time 2026-07-03 05:48:09 +00:00
6 minutes 1 second
igor removed their assignment 2026-07-03 05:48:09 +00:00
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Total time spent: 6 minutes 1 second
igor
6 minutes 1 second
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
joshtronic/igor#329
No description provided.