This project sets up a backend proxy for Google Analytics 4 (GA4) reporting, using a Node.js + Express server. The backend securely interacts with the GA4 Data API via a service account, allowing frontend clients or tools like Postman to request analytics reports using custom filters, dimensions, and metrics.
- Node.js / Express
@google-analytics/data
(GA4 API SDK)google-auth-library
(for service account auth)
index.js
→ Express serverservice-account.json
→ Downloaded from GCP (contains private key, email, etc.)package.json
→ Node dependencies
- Create a service account in Google Cloud Console.
- Generate a JSON key file for this service account.
- Grant the account GA4 Viewer access:
- Go to GA4 Admin → Property Access Management
- Add the service account email with Viewer role
- Place the
.json
key file in the root directory (/GA4-DEMO
)
-
Custom Dimensions Setup
- Register any used dimensions (e.g.,
platform_portal
) - Scope:
Event
,User
, orItem
depending on intended use - Parameter name must match what’s sent in the correct scope (event param, user property, or item param)
- Register any used dimensions (e.g.,
-
Event Collection Validation
- Ensure data is being sent with
gtag()
or GTM - Confirm data exists in recent date ranges (use Free Form UI)
- Ensure data is being sent with
POST /report
{
"startDate": "7daysAgo",
"endDate": "today",
"dimensions": ["customEvent:platform_portal", "city", "deviceCategory"],
"metrics": ["eventCount", "activeUsers"],
"filters": [
{ "field": "city", "value": "Casablanca" },
{ "field": "deviceCategory", "value": "desktop" }
]
}
dimensions[]
– GA4 dimensions (registered if custom)metrics[]
– GA4 metrics (e.g.,eventCount
,activeUsers
)filters[]
– array of dimension-level filtersorderBys[]
– optional metric sort (planned support)
- Client sends a
POST /report
request with JSON payload - Backend reads:
startDate
,endDate
dimensions[]
,metrics[]
- Optional filters
- Backend uses
BetaAnalyticsDataClient.runReport()
to query GA4 - Response is returned to client in raw GA4 API format
- Client (frontend/Postman) reshapes or renders the data
service-account.json
should never be committed or exposed- Use
.env
or secret management in production - Whitelist allowed dimensions/metrics to prevent misuse
+------------------+ +-----------------------+ +------------------------------+
| Frontend App | | Backend Proxy | | GA4 Data API |
| (or Postman Req) | POST | Express + Service Acc| Auth + | analyticsdata.googleapis |
+--------+---------+ --------> +-----------+-----------+ ---------> +--------------+---------------+
| | |
| JSON payload: | Validates dimensions, |
| - startDate, endDate | filters, and metrics |
| - dimensions, metrics | |
| - optional filters | Generates GA4 API request |
| | |
| | Fetches GA4 report |
| | <------------------------------+
| | |
| Receives report JSON | |
| <-------------------------------+ |
| Displays, reshapes, visualizes | |
+--------+---------+ | |
| Client | | |
+------------------+ +---------------------------------+
Use Case | Description |
---|---|
💻 Dashboarding | Dynamically fetch GA4 metrics from different dimensions |
🧪 Postman Testing | Manual analytics report exploration |
🧑💼 Client-specific filtering | Filter results by platform, city, or custom identifiers |
📊 Pivot-style tables | Client-side grouping by deviceCategory , platform_portal , etc. |
🔄 Caching (future) | Optimize repeated queries with in-memory or Redis cache |
🔒 Role-based field control | Lock down dimensions/metrics per client or session (planned) |
The backend supports advanced dimension-level filtering using the GA4 Data API’s dimensionFilter
field. Supported methods include:
"filters": [
{ "field": "city", "value": "Casablanca" }
]
"filters": [
{ "field": "city", "value": "Casablanca" },
{ "field": "deviceCategory", "value": "desktop" }
]
"filters": [
{ "field": "city", "value": "casa", "matchType": "CONTAINS", "caseSensitive": false }
]
Match Type | Description |
---|---|
EXACT |
Exact string match (default) |
BEGINS_WITH |
Value starts with filter |
CONTAINS |
Value contains filter |
You can combine any number of these filters and the backend will construct a GA4-compatible andGroup
dimension filter block.
To successfully query custom dimensions, you must use the correct prefix depending on the dimension's scope:
Scope | API Format Example |
---|---|
Event | customEvent:platform_portal |
User | customUser:user_type |
Item | customItem:item_category |
"dimensions": ["customEvent:platform_portal", "customUser:membership_status", "customItem:category_name"]
- Native dimensions (like
city
,deviceCategory
) are passed unchanged - Custom dimensions are auto-prefixed with the appropriate scope if not already present
- By default, the backend assumes
customEvent:
for unknown fields unless configured otherwise
To override this, pass explicitly prefixed dimension names or configure scope handling logic inside the backend.
You can confirm available custom dimensions via the GA4 Metadata API, which returns all valid dimensions and metrics for a given property.