GA4 Reports — Improvement & Growth Plan
Package: capell-app/ga4-reports · Kind: package · Tier: premium · Product group: Capell Growth · Bundle: growth · Status: Complete
1. Snapshot
Section titled “1. Snapshot”GA4 Reports is a snapshot-based Google Analytics 4 reporting package: a scheduled/CLI sync (SyncGA4ReportsMetricsAction → capell:ga4-reports-sync, with ga4-reports:sync retained as an alias) pulls a date window from the GA4 Data API through a hand-rolled service-account client (src/Support/Insights/GA4ReportsDataClient.php, RS256 JWT auth, persisted cache-store token cache, paginated page reports) and persists into three local tables (ga4_reports_daily_metrics, ga4_reports_page_metrics, ga4_reports_sync_runs). Admin surfaces — one extension GA4ReportsPage, five widgets (overview stats, traffic trend, top pages, top-pages table, setup status), three dashboard overview stats, and a settings group — read only from local rows via Build*Action DTO builders, never hitting GA4 during render. Surfaces declared: admin, console; deps capell-app/admin, capell-app/core plus lorisleiva/laravel-actions + spatie/laravel-data. Current marketplace summary now leads with GA4 traffic, top-page, and conversion snapshots inside Capell admin. Manifest declares the extension card plus 3 committed admin PNG captures from docs/screenshots.json.
Completed Improvement Slices
Section titled “Completed Improvement Slices”- 2026-06-04: Added bounded retry/backoff configuration for GA4 token and Data API calls, including transient connection failures, GA4 quota exhaustion responses,
Retry-After, and explicit final quota-exhaustion messaging. - 2026-06-05: Shipped the GA4 Reports cleanup slice: the service provider now falls back to
NullGA4ReportsDataClientuntil enabled/property/credentials settings are complete, the stray “GA4 Reports 4” copy is removed from composer/README/lang/command text, and the orphan settings page is absent because settings are managed through the registeredga4_reportssettings surface. Evidence:tests/Feature/Package/GA4ReportsPackageTest.phpcovers null/real client binding;src/Providers/AdminServiceProvider.phpregistersGA4ReportsPageplus the settings management surface only; focused typo checks covercomposer.json,README.md,resources/lang/en/package.php, andsrc/Console/Commands/SyncGA4ReportsCommand.php. - 2026-06-08: Added digest/export Actions so local GA4 snapshot data can be assembled once and exported as CSV without hitting GA4 during admin/report rendering.
2. Improvements (existing functionality)
Section titled “2. Improvements (existing functionality)”- Surface
averageSessionDurationandeventCount, or stop computing them —BuildGA4ReportsOverviewAction::averageSessionDuration()runs an extra full->get()load on every overview render to weight a value no widget or overview stat ever displays;conversions/eventCountare persisted buteventCountis never shown. Either add an “Avg. session duration” / “Events” metric row toGA4ReportsOverviewStatsWidgetor drop the computation. —src/Actions/BuildGA4ReportsOverviewAction.php,src/Filament/Widgets/GA4ReportsOverviewStatsWidget.php— S - Shipped 2026-06-06: dashboard read aggregates are cached.
BuildGA4ReportsOverviewAction,BuildGA4ReportsTrendAction, andBuildTopGA4ReportsPagesActionnow cache local aggregate DTOs under a short TTL keyed byproperty_id, date window, and top-page limit, with successful syncs invalidating the dashboard cache.capell.jsonnow declares thega4-reportscache tag and marks the admin aggregate reads cache-safe because they only render local metric snapshots. —src/Support/GA4ReportsDashboardCache.php,src/Actions/BuildGA4ReportsOverviewAction.php,BuildGA4ReportsTrendAction.php,BuildTopGA4ReportsPagesAction.php,SyncGA4ReportsMetricsAction.php,capell.json— M - Shipped 2026-06-06: overview stats use the dashboard default date range.
AdminServiceProvider::ga4Overview()now builds the same current-week dashboard window used by GA4 dashboard widgets before callingBuildGA4ReportsOverviewAction, so the three registered overview stats no longer fall back to the packagesync_dayswindow. Focused coverage seeds a large out-of-window metric and asserts the registered sessions stat only resolves current dashboard-window data. —src/Providers/AdminServiceProvider.php,tests/Feature/Filament/GA4ReportsFilamentTest.php— S - Shipped 2026-06-06: command follows the
capell:convention.SyncGA4ReportsCommandnow exposescapell:ga4-reports-sync, keepsga4-reports:syncas a backward-compatible alias, the schedule uses the Capell command name, andcapell.json.commands.doctorpoints at the sync command for discoverability. —src/Console/Commands/SyncGA4ReportsCommand.php,src/Providers/AdminServiceProvider.php,capell.json— S - Shipped 2026-06-06: configurable sync schedule.
registerSchedule()now readsGA4ReportsSettings::$sync_cronwith a config fallback and calls->cron(...)instead of hard-coded->daily(). The settings schema exposes a translated cron expression field and the package config defaults to0 2 * * *. —src/Providers/AdminServiceProvider.php,src/Settings/GA4ReportsSettings.php,config/capell-ga4-reports.php— S - Mark
credentials_pathas a path field, validate existence — settingsTextInputhas no validation; an unreadable/missing path only surfaces as a runtimeGA4ReportsApiExceptiondeep in a sync run. Add a “file exists & is JSON service account” validation/affordance inGA4ReportsSettingsSchemaand reflect it in setup status. —src/Filament/Settings/GA4ReportsSettingsSchema.php,src/Filament/Widgets/GA4ReportsSetupStatusWidget.php— M - Done/Shipped: add a “Sync now” action to the page/setup widget. —
GA4ReportsPagenow exposes a translatedSync nowheader action that runsSyncGA4ReportsMetricsActionand shows the returned success/failure message in a Filament notification. —src/Filament/Pages/GA4ReportsPage.php— M - Done/Shipped: surface the last sync error to operators. —
GA4ReportsSetupStatusWidgetnow adds a translatedLast errorrow when the latest sync run failed and has a stored error message, capped to a short admin-safe summary. —src/Filament/Widgets/GA4ReportsSetupStatusWidget.php— S
3. Missing Features (gaps)
Section titled “3. Missing Features (gaps)”Capabilities declared: ga4-reports, ga4-reports-admin, ga4-reports-console. Against GA4-integration norms:
- API quota / 429 handling + retry-with-backoff shipped.
GA4ReportsDataClient::runReport()andaccessToken()now retry transient connection failures, common upstream statuses,429, and GA4RESOURCE_EXHAUSTEDresponses with bounded backoff andRetry-Aftersupport. Final quota failures now raise an explicit quota-exhaustion message. Keep the focused data-client tests as regression coverage. —src/Support/Insights/GA4ReportsDataClient.php,tests/Unit/Support/GA4ReportsDataClientTest.php - Shipped 2026-06-06: persisted / cross-process token cache.
GA4ReportsDataClientnow caches OAuth access tokens in the configured cache store under a hashed service-account/scope key, honoursexpires_in, and keeps a 60-second refresh buffer so queue workers and CLI invocations avoid unnecessary JWT signing/token requests. —src/Support/Insights/GA4ReportsDataClient.php,config/capell-ga4-reports.php,tests/Unit/Support/GA4ReportsDataClientTest.php - Service-account is the only auth path. Norm is to also support OAuth user consent (Search Console / GA4 connect-flow) so non-technical owners avoid hand-placing a JSON key file. The contract (
GA4ReportsDataClientInterface) makes this swappable, but no OAuth client ships. Differentiator vs table-stakes. - Single property only.
property_idis a scalar setting; no multi-property selection or property picker, and the schema indexes already key onproperty_id, so multi-property is half-built. Multi-property is a clear premium differentiator for agencies/multi-site owners. - Shipped 2026-06-06: period-over-period widget comparisons. The GA4 dashboard widgets now derive the immediately preceding window from the selected dashboard date range, show previous-period datasets on the traffic trend chart, add
Vs previousdeltas to overview rows, and compare top-page views against the matching previous-period page path. —src/Filament/Widgets/Concerns/BuildsGA4ReportsDashboardWindow.php,src/Filament/Widgets/GA4ReportsTrafficTrendWidget.php,src/Filament/Widgets/GA4ReportsOverviewStatsWidget.php,src/Filament/Widgets/GA4ReportsTopPagesWidget.php,src/Filament/Widgets/GA4ReportsTopPagesTableWidget.php - No realtime / no events or conversions breakdown widget.
eventCountandconversionsare stored but onlyconversionsappears (as a column); there is no events-by-name or conversions-by-event widget, and no GA4 realtime card. Table stakes for “GA4 reporting”. - Shipped 2026-06-08: CSV digest/export seam.
BuildGA4ReportsDigestActionassembles overview, trend, and top-page local snapshot data;ExportGA4ReportsDigestCsvActionserializes that digest to CSV for admin commands, future dashboard downloads, or scheduled-report consumers. PDF output and scheduled email delivery remain future depth. - Dimensions are fixed. Reports are hardcoded to
date/pagePathPlusQueryString/pageTitle; no channel/source-medium, country, or device dimension. No configurable report builder. Differentiator if added.
4. Issues / Risks
Section titled “4. Issues / Risks”- Null-client binding shipped.
GA4ReportsServiceProvider::bindGA4ReportsClient()now bindsNullGA4ReportsDataClientuntil enabled, property ID, and credentials path are all configured, keeping the README and data-client docs accurate. Regression evidence lives intests/Feature/Package/GA4ReportsPackageTest.php. —src/Support/Insights/NullGA4ReportsDataClient.php,src/Providers/GA4ReportsServiceProvider.php - Documentation mismatch closed by null-client binding.
docs/data-client.mdstates “The package binds a real client when config is complete and a null client when it is not”, andREADME.mdlists “null-client behavior” as a value prop; the provider binding now matches those claims. —docs/data-client.md,README.md,src/Providers/GA4ReportsServiceProvider.php - Health check shipped.
Ga4ReportsHealthChecknow runs real Diagnostics probes for storage tables, model/schema availability, credential/settings readiness, latest successful sync freshness, and data-client/API read reachability where an enabled install has local credentials ready. Output intentionally avoids echoing the GA4 property ID, private key, credential JSON, or full credentials path. Evidence:vendor/bin/pest packages/ga4-reports/tests/Feature/Health/GA4ReportsHealthCheckTest.php --configuration=phpunit.xml(14 tests / 38 assertions). —src/Health/Ga4ReportsHealthCheck.php,tests/Feature/Health/GA4ReportsHealthCheckTest.php,config/capell-ga4-reports.php - Orphan settings page removed. Settings are surfaced via
registerExtensionManagementSurface+ thega4_reportssettings group, and onlyGA4ReportsPageis registered as an extension page. NoGA4ReportsSettingsPagefile remains in the package. —src/Providers/AdminServiceProvider.php - Limited caching resilience remains (see §3) — transient GA4 5xx/429 failures now retry before the sync run fails, and access tokens are cached cross-process in the configured cache store. The sync schedule is still daily by default. —
src/Support/Insights/GA4ReportsDataClient.php,src/Providers/AdminServiceProvider.php - Bound client coverage added; real sync-path exception coverage remains.
GA4ReportsDataClientTestexercisesGA4ReportsDataClientdirectly, andtests/Feature/Package/GA4ReportsPackageTest.phpnow asserts the container binds the null client when unconfigured and the real client when configured.SyncGA4ReportsMetricsActionstill usesFakeGA4ReportsDataClientfor lifecycle/error-path tests. —tests/Feature/Actions/GA4ReportsActionsTest.php,tests/Feature/Package/GA4ReportsPackageTest.php - Credential/secret handling. The service-account JSON (containing a private key) lives at an operator-set filesystem path;
credentials_pathis stored in the settings DB and rendered as a plainTextInput(path disclosure, nopassword()/masking). The client reads the file each token refresh with anis_readableguard and throws typed exceptions — acceptable — but secrets should never be echoed; confirm the path field and any error surfacing never leak the file contents, and mask the path in the UI. Public-output safety is not a concern here (admin-only surfaces, no frontend) but the capell “never leak API credentials” rule still applies to admin error messages. —src/Filament/Settings/GA4ReportsSettingsSchema.php,src/Support/Insights/GA4ReportsDataClient.php - Performance cache metadata shipped.
capell.json performancenow keepsfrontendRenderBudgetMs: 0for the admin-only package, declares thega4-reportscache tag, and marks local aggregate dashboard reads cache-safe by property/date range/limit with invalidation from sync/local metric persistence. —capell.json,src/Support/GA4ReportsDashboardCache.php commercial.privateDocsRequested: truebutdocs/is public-style. The README links siblings and renders public OpenGraph preview cards; confirm what is intended to be private vs marketplace-public. —capell.json,README.md- Done/Shipped: Branding typo “GA4 Reports 4”. The stray “4” copy has been removed from composer, README, language, and command-description surfaces; focused
rgchecks now find no shipped-code copy matches. —composer.json,README.md,resources/lang/en/package.php,src/Console/Commands/SyncGA4ReportsCommand.php - PHP floor lags house standard.
composer.jsonrequiresphp: ^8.3; the capell convention targets PHP 8.4. Low risk (code is 8.3-compatible) but inconsistent with siblings. —composer.json - i18n. Strings are translated via
capell-ga4-reports::keys acrosswidgets,settings,sync(good). Gaps:Ga4ReportsHealthCheckand theMarketingStudioActionData/ExtensionManagementSurfaceDataicons use raw strings (acceptable), but the README/docs typo and the persisted enum-likestatusstrings (running/succeeded/failed) are raw literals in the model/sync action rather than a backed enum with labels — the capell convention prefers backed enums for persisted values. —src/Actions/SyncGA4ReportsMetricsAction.php,src/Filament/Widgets/GA4ReportsSetupStatusWidget.php
5. Marketplace & Selling
Section titled “5. Marketplace & Selling”Marketplace copy status: manifest, Composer, README, and overview now lead with the concrete value: pulling Google Analytics 4 traffic, top-page, and conversion snapshots into Capell admin on a daily schedule without per-pageview API calls. The remaining buyer-facing gap is visual proof of the reporting surfaces.
Improved 1-sentence summary:
Pull Google Analytics 4 traffic, top-page, and conversion snapshots into your Capell admin on a daily schedule — no per-pageview API calls, no leaving the CMS.
Improved 3–4 sentence description:
GA4 Reports brings Google Analytics 4 into the Capell admin as cached daily snapshots, so owners see traffic trends, top pages, sessions, and conversions beside the content they manage. A scheduled sync authenticates with a Google service account, stores a configurable window locally, and powers dashboard widgets and overview stats that never call GA4 at render time. Setup status, sync history, and a swappable data-client contract keep the integration transparent and testable. Built for marketing and growth teams who want analytics signal in the CMS without standing up a separate reporting tool.
Screenshot/media status: Manifest ships the marketplace card plus all three committed admin PNG captures from docs/screenshots.json (dashboard page, setup-status, settings). Remaining optional media depth is a tighter hero crop of the traffic-trend chart for the listing.
Pricing / tier / bundle positioning: premium tier, Capell Growth group, growth bundle — correct. It sits alongside insights (also Growth/growth bundle) which records first-party visits/events/journeys; GA4 Reports is the third-party analytics counterpart. Position GA4 Reports as the “bring your existing GA4 in” on-ramp and insights as the “own your first-party data” upgrade.
Cross-sell (via deps + Extension Suites):
- insights — same bundle; “first-party vs GA4” complementary pairing; bundle discount.
- dashboard-reports (Operations) — generic reporting widgets that can host GA4 cards.
- seo-suite (Search & SEO) — already has Search Console insights; cross-sell as a “full acquisition picture” (organic search + GA4 behaviour).
- The shipped CSV digest/export seam is a natural hook into a marketing/email package for scheduled reporting depth.
Differentiators / value props / target buyer: Snapshot architecture (zero GA4 calls at render → fast, quota-safe dashboards); swappable GA4ReportsDataClientInterface (fake in tests, OAuth/alternate backend in host); admin-native, no third-party BI tool. Target buyer: marketing/growth lead or site owner on a Capell site who already runs GA4 and wants the headline numbers inside the CMS.
Keywords/tags: ga4, google-analytics, analytics, traffic-reports, dashboard, top-pages, conversions, marketing-analytics, growth, service-account, scheduled-sync, filament
Completion Review
Section titled “Completion Review”Completed 2026-06-08. The current manifest-backed plan is closed: GA4 Reports ships local snapshot syncing, null-client protection, retry/backoff and quota handling, persisted token cache, configurable schedules, setup/error visibility, dashboard aggregate caching, period-over-period widgets, real health diagnostics, runner-backed screenshots, and CSV digest/export Actions. OAuth connect flow, multi-property selection, realtime/events breakdowns, PDF/scheduled emails, and configurable report dimensions remain future product-depth candidates.
6. Prioritized Roadmap
Section titled “6. Prioritized Roadmap”| Item | Bucket | Effort | Impact | Section |
|---|---|---|---|---|
| Fix client binding to fall back to null client when unconfigured (or delete null client + claims) | Shipped | S | High | §4 |
Implement real Ga4ReportsHealthCheck (credentials, last-sync recency, API reachability) | Shipped | M | High | §4 |
| Add retry-with-backoff + 429/quota handling to GA4 HTTP calls | Done | M | High | §3, §4 |
| Fix “GA4 Reports 4” typo across composer/README/lang/command | Shipped | S | Med | §4 |
| Generate & commit the 3 required screenshots | Done | S | High | §5 — closed 2026-06-06: three Capell runner PNG captures are committed and promoted into marketplace media. |
| Shipped: rewrite marketplace summary + description | Done | S | High | §5 |
Remove orphan GA4ReportsSettingsPage (or wire it) | Shipped | S | Med | §4 |
Shipped 2026-06-06: Cache dashboard read aggregates; set cacheTags + revisit cacheSafety in manifest | Done | M | High | §2, §4 — local aggregate DTO cache added with sync invalidation and manifest cache metadata. |
| Shipped 2026-06-06: Make overview stats honour the dashboard default date range | Done | S | Med | §2 |
| Shipped 2026-06-06: Add period-over-period comparison (deltas) to widgets | Done | M | High | §3 — overview rows, traffic trend datasets, and top-pages rows compare against the immediately preceding dashboard window. |
| Shipped 2026-06-06: Persisted/cross-process OAuth token cache | Done | S | Med | §3 |
| Done/Shipped: Add “Sync now” page action + surface last sync error | Done | M | Med | §2 |
Rename command to capell: convention; populate manifest commands | Done | S | Med | §2 — Done 2026-06-06: primary command is capell:ga4-reports-sync; legacy ga4-reports:sync remains an alias. |
| Shipped 2026-06-06: Configurable sync schedule (frequency/cron via settings) | Done | S | Med | §2 |
| Shipped 2026-06-08: CSV digest/export Action seam | Done | M | Med | §3 |
| Multi-property support + property picker | Future | L | High | §3 |
| OAuth user-consent connect flow (alongside service account) | Future | L | High | §3 |
| Events/conversions breakdown widget + PDF export & scheduled digest | Future | L | Med | §3 |
| Configurable report dimensions (channel, country, device) | Future | L | Med | §3 |
| Shipped: Test that the container binds the real client when configured | Done | S | Med | §4 |