<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: YunarW</title>
    <description>The latest articles on DEV Community by YunarW (@yunarw).</description>
    <link>https://dev.to/yunarw</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F292108%2F94f6e30f-fc48-43d9-b590-229acb3f27f0.png</url>
      <title>DEV Community: YunarW</title>
      <link>https://dev.to/yunarw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yunarw"/>
    <language>en</language>
    <item>
      <title>How I Automated GA4 and Google Search Console Reporting With Python</title>
      <dc:creator>YunarW</dc:creator>
      <pubDate>Thu, 08 Oct 2026 09:14:14 +0000</pubDate>
      <link>https://dev.to/yunarw/how-i-automated-ga4-and-google-search-console-reporting-with-python-77l</link>
      <guid>https://dev.to/yunarw/how-i-automated-ga4-and-google-search-console-reporting-with-python-77l</guid>
      <description>&lt;p&gt;Every month, I used to repeat the same reporting routine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open Google Analytics 4&lt;/li&gt;
&lt;li&gt;open Google Search Console&lt;/li&gt;
&lt;li&gt;change the date range&lt;/li&gt;
&lt;li&gt;export the data&lt;/li&gt;
&lt;li&gt;compare it with the previous month&lt;/li&gt;
&lt;li&gt;build charts&lt;/li&gt;
&lt;li&gt;write observations&lt;/li&gt;
&lt;li&gt;repeat everything for another website&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point, I realized that most of this process was not analysis.&lt;br&gt;
It was data collection and formatting.&lt;br&gt;
So I decided to automate it with Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal was simple:&lt;/strong&gt;&lt;br&gt;
Run one command and generate the same monthly website report I would normally build manually.&lt;/p&gt;

&lt;p&gt;This article explains the structure I used, what data I collect, and a few lessons I learned while building it.&lt;/p&gt;

&lt;p&gt;I manage several websites, so I wanted the script to answer a few basic questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many users visited the website?&lt;/li&gt;
&lt;li&gt;How many sessions were recorded?&lt;/li&gt;
&lt;li&gt;How many organic clicks came from Google Search?&lt;/li&gt;
&lt;li&gt;How many impressions did the website receive?&lt;/li&gt;
&lt;li&gt;What was the average CTR?&lt;/li&gt;
&lt;li&gt;What was the average position?&lt;/li&gt;
&lt;li&gt;Which websites improved or declined?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I deliberately avoided building a complicated dashboard.&lt;br&gt;
The first version only needed to produce a consistent monthly report.&lt;br&gt;
The workflow looked like this:&lt;/p&gt;

&lt;p&gt;Google Analytics 4&lt;br&gt;
        ↓&lt;br&gt;
Google Search Console&lt;br&gt;
        ↓&lt;br&gt;
      Python&lt;br&gt;
        ↓&lt;br&gt;
Normalize the data&lt;br&gt;
        ↓&lt;br&gt;
Calculate metrics&lt;br&gt;
        ↓&lt;br&gt;
Generate charts&lt;br&gt;
        ↓&lt;br&gt;
Create PDF report&lt;/p&gt;

&lt;p&gt;The Python Packages&lt;br&gt;
My basic stack uses:&lt;br&gt;
google-api-python-client&lt;br&gt;
google-auth&lt;br&gt;
google-analytics-data&lt;br&gt;
reportlab&lt;br&gt;
matplotlib&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install them with:&lt;/strong&gt;&lt;br&gt;
pip install google-api-python-client google-auth google-analytics-data reportlab matplotlib&lt;/p&gt;

&lt;p&gt;The Google libraries handle the API requests.&lt;br&gt;
matplotlib handles simple charts.&lt;br&gt;
ReportLab generates the final PDF.&lt;br&gt;
Setting Up Authentication&lt;/p&gt;

&lt;p&gt;The first part that took some time was Google authentication.&lt;br&gt;
I use a Google Cloud Service Account.&lt;/p&gt;

&lt;p&gt;The general process is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Google Cloud project.&lt;/li&gt;
&lt;li&gt;Enable the Google Analytics Data API.&lt;/li&gt;
&lt;li&gt;Enable the Search Console API.&lt;/li&gt;
&lt;li&gt;Create a Service Account.&lt;/li&gt;
&lt;li&gt;Download the JSON credentials file.&lt;/li&gt;
&lt;li&gt;Give the Service Account access to GA4.&lt;/li&gt;
&lt;li&gt;Give the same Service Account access to Search Console.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once that is done, the Python script can authenticate without requiring a browser login every time.&lt;br&gt;
A basic authentication setup might look like:&lt;br&gt;
from google.oauth2 import service_accountcredentials = service_account.Credentials.from_service_account_file("service-account.json")&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One lesson I learned here:&lt;/strong&gt;&lt;br&gt;
Keep the credentials file outside your public repository.&lt;br&gt;
Add it to .gitignore:&lt;br&gt;
service-account.json&lt;/p&gt;

&lt;p&gt;Never commit API credentials to GitHub.&lt;br&gt;
Pulling Data From GA4&lt;br&gt;
For Google Analytics 4, I use the Analytics Data API.&lt;/p&gt;

&lt;p&gt;Here is a simplified example:&lt;br&gt;
from google.analytics.data_v1beta import BetaAnalyticsDataClientfrom google.analytics.data_v1beta.types import (DateRange,Metric,  RunReportRequest,)client = BetaAnalyticsDataClient(credentials=credentials)request = RunReportRequest(property="properties/123456789",metrics=[        Metric(name="activeUsers"),Metric(name="sessions"),],date_ranges=[        DateRange(start_date="2026-09-01",end_date="2026-09-30")])response = client.run_report(request)&lt;/p&gt;

&lt;p&gt;Then I extract the returned values:&lt;br&gt;
row = response.rows[0]active_users = int(row.metric_values[0].value)sessions = int(row.metric_values[1].value)&lt;/p&gt;

&lt;p&gt;This gives me two of the main metrics I need.&lt;br&gt;
Why I Keep GA4 Metrics Simple&lt;br&gt;
GA4 exposes many metrics.&lt;br&gt;
It is tempting to pull everything.&lt;br&gt;
I tried that approach at first.&lt;br&gt;
It made the report worse.&lt;br&gt;
More data created more noise.&lt;br&gt;
For my monthly report, I prefer to start with:&lt;br&gt;
Active Users&lt;br&gt;
Sessions&lt;/p&gt;

&lt;p&gt;Then add more only when there is a clear reason.&lt;br&gt;
For example:&lt;/p&gt;

&lt;p&gt;Engaged Sessions&lt;br&gt;
Conversions&lt;br&gt;
Event Count&lt;br&gt;
Views&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule I use now is:&lt;/strong&gt;&lt;br&gt;
If a metric does not change a decision, it probably does not need to be in the main report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pulling Search Console Data&lt;/strong&gt;&lt;br&gt;
Search Console gives me the organic search side.&lt;br&gt;
A basic request looks like this:&lt;br&gt;
from googleapiclient.discovery import buildservice = build(    "searchconsole","v1",credentials=credentials)request = {"startDate": "2026-09-01","endDate":"2026-09-30","searchType": "web"}response=(    service.searchanalytics().query(siteUrl="https: //example.com/",        body=request).execute())&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From Search Console, I collect:&lt;/strong&gt;&lt;br&gt;
Clicks&lt;br&gt;
Impressions&lt;br&gt;
CTR&lt;br&gt;
Average Position&lt;/p&gt;

&lt;p&gt;If I want page-level data, I add dimensions:&lt;br&gt;
request = {"startDate": "2026-09-01","endDate": "2026-09-30",    "dimensions": ["page"],"rowLimit": 25000}&lt;/p&gt;

&lt;p&gt;For keyword-level data:&lt;br&gt;
"dimensions": ["query"]&lt;/p&gt;

&lt;p&gt;This makes the same API useful for both executive reporting and deeper SEO analysis.&lt;br&gt;
Normalizing the Data&lt;br&gt;
GA4 and Search Console return different response structures.&lt;br&gt;
Before generating a report, I convert everything into a common Python dictionary.&lt;br&gt;
For example:&lt;br&gt;
website_data = {"website": "example.com","users": 24500,"sessions": 31800,    "clicks": 9200,"impressions": 210000,"ctr": 4.38,"position": 9.2}&lt;/p&gt;

&lt;p&gt;Then multiple websites become:&lt;br&gt;
websites = [website_1,website_2,website_3]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This step is important.&lt;/strong&gt;&lt;br&gt;
Once the data has a consistent structure, the rest of the program becomes much easier.&lt;br&gt;
Charts do not care whether the original metric came from GA4 or Search Console.&lt;/p&gt;

&lt;p&gt;The PDF generator does not need to know either.&lt;br&gt;
It only reads the normalized data.&lt;br&gt;
Calculating Changes&lt;br&gt;
Raw numbers are useful, but changes are usually more interesting.&lt;/p&gt;

&lt;p&gt;A simple percentage change function:&lt;br&gt;
def percentage_change(current, previous):    if previous == 0:        return None    return ((current - previous) / previous) * 100&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
previous_clicks = 10000current_clicks = 8500change = percentage_change(    current_clicks,previous_clicks)print(change)&lt;/p&gt;

&lt;p&gt;Output:&lt;br&gt;
-15.0&lt;/p&gt;

&lt;p&gt;Now the report can say:&lt;br&gt;
Organic clicks decreased by 15%.&lt;/p&gt;

&lt;p&gt;That is more useful than simply showing:&lt;br&gt;
Clicks: 8,500&lt;/p&gt;

&lt;p&gt;Adding Simple Rules&lt;br&gt;
I also wanted the report to highlight things worth investigating.&lt;br&gt;
I started with very simple rules.&lt;br&gt;
Example:&lt;br&gt;
def analyze_seo(impressions, ctr, position):notes = []if impressions &amp;gt; 1000 and ctr &amp;lt; 2:notes.append("High visibility but low CTR."        )    if 4 &amp;lt;= position &amp;lt;= 10:notes.append("Page-one ranking opportunity."        )return notes&lt;/p&gt;

&lt;p&gt;This does not replace SEO analysis.&lt;br&gt;
It simply points me toward possible opportunities.&lt;br&gt;
The distinction matters.&lt;br&gt;
I do not want the script to say:&lt;br&gt;
Traffic dropped because of a Google algorithm update.&lt;/p&gt;

&lt;p&gt;The available data cannot prove that.&lt;br&gt;
It can say:&lt;br&gt;
Clicks declined by 30% while impressions stayed stable.&lt;/p&gt;

&lt;p&gt;That is a factual signal.&lt;br&gt;
A person can investigate the reason.&lt;br&gt;
Generating Charts&lt;br&gt;
For simple report charts, Matplotlib is enough.&lt;br&gt;
Example:&lt;br&gt;
import matplotlib.pyplot as pltsites = ["Site A","Site B","Site C"]clicks = [8200,5100,3400]plt.bar(sites, clicks)plt.title("Organic Clicks")plt.ylabel("Clicks")plt.savefig("organic-clicks.png",    bbox_inches="tight")plt.close()&lt;/p&gt;

&lt;p&gt;I intentionally keep the visualizations simple.&lt;br&gt;
The report is meant to answer questions, not demonstrate how many chart types Matplotlib supports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating the PDF&lt;/strong&gt;&lt;br&gt;
For the PDF report, I use ReportLab.&lt;br&gt;
A very basic example:&lt;br&gt;
from reportlab.lib.pagesizes import A4from reportlab.platypus import (    SimpleDocTemplate,Paragraph,Spacer)from reportlab.lib.styles import getSampleStyleSheetstyles = getSampleStyleSheet()document = SimpleDocTemplate("monthly-report.pdf",pagesize=A4)content = []content.append(Paragraph("Monthly Website Performance Report",        styles["Title"]))content.append(Spacer(1, 20))content.append(    Paragraph("This report contains GA4 and Google Search Console data.",        styles["BodyText"]))document.build(content)&lt;/p&gt;

&lt;p&gt;From there I can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tables&lt;/li&gt;
&lt;li&gt;charts&lt;/li&gt;
&lt;li&gt;page breaks&lt;/li&gt;
&lt;li&gt;recommendations&lt;/li&gt;
&lt;li&gt;website summaries
The final PDF is generated automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supporting Multiple Websites&lt;br&gt;
The next step was making the script work across multiple websites.&lt;br&gt;
I created a simple configuration structure:&lt;br&gt;
sites = [{"name": "Site A","ga4_property": "123456789","gsc_url":"https ://site-a.com/"},{"name": "Site B","ga4_property": "987654321",        "gsc_url": "https ://site-b.com/"}]&lt;/p&gt;

&lt;p&gt;Then the script loops through them for site in sites:&lt;br&gt;&lt;br&gt;
ga4_data = get_ga4_data(site["ga4_property"])gsc_data = get_gsc_data(site["gsc_url"])generate_site_report(site,        ga4_data,gsc_data)&lt;/p&gt;

&lt;p&gt;This was the point where the project became genuinely useful for me.&lt;br&gt;
Instead of running five different reporting workflows, I had one.&lt;br&gt;
Error Handling Matters More Than I Expected&lt;br&gt;
APIs fail.&lt;br&gt;
Credentials expire.&lt;br&gt;
Properties are misconfigured.&lt;br&gt;
Sometimes a website has no data for a period.&lt;br&gt;
My first version assumed everything would work.&lt;br&gt;
That was a mistake.&lt;br&gt;
Now I prefer explicit error handling:&lt;br&gt;
try: ga4_data = get_ga4_data(property_id)except Exception as error:    print(f"GA4 error: {error}")ga4_data = None&lt;/p&gt;

&lt;p&gt;The report should still be generated even if one data source fails.&lt;br&gt;
Instead of crashing, it can display:&lt;br&gt;
GA4 data unavailable.&lt;br&gt;
Check API access or property configuration.&lt;/p&gt;

&lt;p&gt;This makes the automation much more usable in real work.&lt;br&gt;
One Command Instead of a Monthly Routine&lt;br&gt;
The old workflow looked like this:&lt;/p&gt;

&lt;p&gt;Open GA4&lt;br&gt;
↓&lt;br&gt;
Export report&lt;br&gt;
↓&lt;br&gt;
Open Search Console&lt;br&gt;
↓&lt;br&gt;
Export report&lt;br&gt;
↓&lt;br&gt;
Clean CSV&lt;br&gt;
↓&lt;br&gt;
Create charts&lt;br&gt;
↓&lt;br&gt;
Calculate metrics&lt;br&gt;
↓&lt;br&gt;
Build PDF&lt;br&gt;
↓&lt;br&gt;
Repeat&lt;/p&gt;

&lt;p&gt;Now it looks more like:&lt;br&gt;
python generate_report.py&lt;/p&gt;

&lt;p&gt;The script:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;authenticates with Google;&lt;/li&gt;
&lt;li&gt;retrieves GA4 data;&lt;/li&gt;
&lt;li&gt;retrieves Search Console data;&lt;/li&gt;
&lt;li&gt;calculates metrics;&lt;/li&gt;
&lt;li&gt;generates charts;&lt;/li&gt;
&lt;li&gt;creates the PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The part I still do manually is the most important part:&lt;br&gt;
deciding what the numbers mean.&lt;/p&gt;

&lt;p&gt;What I Learned&lt;br&gt;
A few lessons became clear while building this.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Automate repetition, not judgment&lt;br&gt;
APIs are good at collecting data.&lt;br&gt;
Python is good at transforming it.&lt;br&gt;
Neither automatically knows why traffic changed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start with fewer metrics&lt;br&gt;
My first instinct was to collect everything.&lt;br&gt;
A smaller report turned out to be more useful.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Normalize early&lt;br&gt;
Converting API responses into a consistent data structure makes everything after that easier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Make failures visible&lt;br&gt;
Do not silently replace missing API data with zero.&lt;br&gt;
A zero can look like a real metric.&lt;br&gt;
Use explicit statuses such as:&lt;br&gt;
NO DATA&lt;br&gt;
API ERROR&lt;br&gt;
NOT CONFIGURED&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build the smallest useful version first&lt;br&gt;
The project did not need:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;authentication UI&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a web dashboard&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;user accounts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a database&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI-generated insights&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first useful version was simply:&lt;br&gt;
API → Python → PDF&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That was enough.&lt;/strong&gt;&lt;br&gt;
Where I Want to Take It Next&lt;br&gt;
There are a few improvements I would like to build.&lt;br&gt;
Multi-month trend detection&lt;br&gt;
Instead of comparing only two periods, the script could identify longer-term traffic decay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query clustering&lt;/strong&gt;&lt;br&gt;
Search Console queries could be grouped by topic or intent.&lt;br&gt;
Automated anomaly detection&lt;br&gt;
Instead of fixed thresholds, the script could learn what normal traffic looks like for each site.&lt;/p&gt;

&lt;p&gt;Better AI referral tracking&lt;br&gt;
AI assistants are becoming another source of website discovery, but analytics attribution can still be messy.&lt;/p&gt;

&lt;p&gt;Web interface&lt;br&gt;
Eventually, the Python reporting workflow could become a small web application.&lt;br&gt;
But I would still keep the same basic architecture:&lt;/p&gt;

&lt;p&gt;Data sources&lt;br&gt;
↓&lt;br&gt;
Collection&lt;br&gt;
↓&lt;br&gt;
Normalization&lt;br&gt;
↓&lt;br&gt;
Analysis&lt;br&gt;
↓&lt;br&gt;
Presentation&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source Code vs. The Idea&lt;/strong&gt;&lt;br&gt;
I wrote a more detailed guide about my actual setup, including Google Service Account configuration and the reporting workflow, here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wisetechlist.com/how-to-automate-ga4-gsc-report/" rel="noopener noreferrer"&gt;How to Automate GA4 + Google Search Console Reports&lt;/a&gt;&lt;br&gt;
The interesting part for me, however, was not the exact Python code.&lt;br&gt;
It was realizing how much time I was spending on a process that followed exactly the same steps every month.&lt;/p&gt;

&lt;p&gt;Once a workflow becomes predictable enough to write as:&lt;br&gt;
Step A&lt;br&gt;
Step B&lt;br&gt;
Step C&lt;br&gt;
Step D&lt;/p&gt;

&lt;p&gt;there is a good chance some of it can be automated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;br&gt;
This project started because I did not want to build five website reports manually every month.&lt;br&gt;
It ended up changing how I think about internal tools.&lt;br&gt;
You do not always need to build a SaaS product.&lt;br&gt;
You do not always need a complex dashboard.&lt;br&gt;
Sometimes a small Python script that solves one repetitive problem is enough.&lt;/p&gt;

&lt;p&gt;The current workflow is not meant to replace Google Analytics or Search Console.&lt;/p&gt;

&lt;p&gt;It simply removes the repetitive steps between data exists and:&lt;br&gt;
I can actually analyze it&lt;/p&gt;

&lt;p&gt;And for me, that was worth automating.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
