Scenario
Your desktop and mobile users behave differently — session lengths, engagement patterns, scroll depth, and conversion rates all vary by device. You pull device category, screen resolution, and engagement metrics from GA4, then send the behavioral breakdown to Mave for creative format recommendations. The result tells you which ad formats, content layouts, and creative dimensions to prioritize for each device segment.Architecture
Code
import os, requests
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
RunReportRequest, Dimension, Metric, DateRange, OrderBy,
)
PROPERTY_ID = os.environ["GA4_PROPERTY_ID"]
MV = os.environ["MAVERA_API_KEY"]
client = BetaAnalyticsDataClient()
device_report = client.run_report(RunReportRequest(
property=f"properties/{PROPERTY_ID}",
dimensions=[
Dimension(name="deviceCategory"),
Dimension(name="screenResolution"),
],
metrics=[
Metric(name="totalUsers"),
Metric(name="sessions"),
Metric(name="engagementRate"),
Metric(name="averageSessionDuration"),
Metric(name="conversions"),
Metric(name="screenPageViewsPerSession"),
],
date_ranges=[DateRange(start_date="30daysAgo", end_date="today")],
order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="totalUsers"), desc=True)],
limit=100,
))
from collections import defaultdict
devices = defaultdict(lambda: {
"users": 0, "sessions": 0, "conversions": 0,
"eng_sum": 0, "dur_sum": 0, "pages_sum": 0,
"resolutions": defaultdict(int),
})
for row in device_report.rows:
cat = row.dimension_values[0].value
res = row.dimension_values[1].value
users = int(row.metric_values[0].value)
sessions = int(row.metric_values[1].value)
engagement = float(row.metric_values[2].value)
duration = float(row.metric_values[3].value)
conversions = int(row.metric_values[4].value)
pages_per = float(row.metric_values[5].value)
devices[cat]["users"] += users
devices[cat]["sessions"] += sessions
devices[cat]["conversions"] += conversions
devices[cat]["eng_sum"] += engagement * users
devices[cat]["dur_sum"] += duration * users
devices[cat]["pages_sum"] += pages_per * sessions
devices[cat]["resolutions"][res] += users
device_block = []
for cat, data in sorted(devices.items(), key=lambda x: -x[1]["users"]):
avg_eng = data["eng_sum"] / max(data["users"], 1)
avg_dur = data["dur_sum"] / max(data["users"], 1)
avg_pages = data["pages_sum"] / max(data["sessions"], 1)
conv_rate = data["conversions"] / max(data["users"], 1)
top_res = sorted(data["resolutions"].items(), key=lambda x: -x[1])[:5]
res_str = ", ".join(f"{r}: {n}" for r, n in top_res)
device_block.append(
f"**{cat.upper()}**\n"
f" Users: {data['users']} | Sessions: {data['sessions']}\n"
f" Engagement: {avg_eng:.0%} | Avg duration: {avg_dur:.0f}s | Pages/session: {avg_pages:.1f}\n"
f" Conversions: {data['conversions']} ({conv_rate:.2%})\n"
f" Top resolutions: {res_str}"
)
device_summary = "\n\n".join(device_block)
mave = requests.post(
"https://app.mavera.io/api/v1/mave/chat",
headers={"Authorization": f"Bearer {MV}", "Content-Type": "application/json"},
json={"message": f"""Recommend creative format adjustments for each device category based on this GA4 behavioral data.
DEVICE BEHAVIORAL PROFILES (last 30 days):
{device_summary}
For each device category, provide:
1. Recommended ad creative dimensions and formats (static, video, carousel, etc.)
2. Optimal content layout (long-form vs. snackable, scroll depth expectations)
3. CTA placement recommendations based on session duration and pages/session
4. Landing page design considerations for the top screen resolutions
5. Content format priorities (video length, image aspect ratio, text density)
6. Specific do's and don'ts for creative on this device
Also provide cross-device recommendations:
- Which messages to keep consistent across devices
- Which elements to adapt per device
- Mobile-first vs. desktop-first content strategy recommendation"""},
).json()
print("--- Device-Specific Creative Recommendations ---")
print(mave.get("content", "")[:3000])
const MV = process.env.MAVERA_API_KEY;
const PROPERTY_ID = process.env.GA4_PROPERTY_ID;
const KEY_FILE = JSON.parse(require("fs").readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, "utf8"));
const { GoogleAuth } = require("google-auth-library");
const auth = new GoogleAuth({
credentials: KEY_FILE,
scopes: ["https://www.googleapis.com/auth/analytics.readonly"],
});
const accessToken = await auth.getAccessToken();
const gaRes = await fetch(
`https://analyticsdata.googleapis.com/v1beta/properties/${PROPERTY_ID}:runReport`,
{
method: "POST",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
dimensions: [{ name: "deviceCategory" }, { name: "screenResolution" }],
metrics: [
{ name: "totalUsers" }, { name: "sessions" },
{ name: "engagementRate" }, { name: "averageSessionDuration" },
{ name: "conversions" }, { name: "screenPageViewsPerSession" },
],
dateRanges: [{ startDate: "30daysAgo", endDate: "today" }],
orderBys: [{ metric: { metricName: "totalUsers" }, desc: true }],
limit: 100,
}),
}
).then((r) => r.json());
const devices = {};
for (const row of gaRes.rows || []) {
const cat = row.dimensionValues[0].value;
const res = row.dimensionValues[1].value;
const users = parseInt(row.metricValues[0].value);
const sessions = parseInt(row.metricValues[1].value);
const eng = parseFloat(row.metricValues[2].value);
const dur = parseFloat(row.metricValues[3].value);
const conv = parseInt(row.metricValues[4].value);
const pps = parseFloat(row.metricValues[5].value);
devices[cat] ??= { users: 0, sessions: 0, conv: 0, engSum: 0, durSum: 0, ppsSum: 0, resolutions: {} };
devices[cat].users += users;
devices[cat].sessions += sessions;
devices[cat].conv += conv;
devices[cat].engSum += eng * users;
devices[cat].durSum += dur * users;
devices[cat].ppsSum += pps * sessions;
devices[cat].resolutions[res] = (devices[cat].resolutions[res] || 0) + users;
}
const deviceBlocks = Object.entries(devices)
.sort(([, a], [, b]) => b.users - a.users)
.map(([cat, d]) => {
const avgEng = d.engSum / (d.users || 1);
const avgDur = d.durSum / (d.users || 1);
const avgPps = d.ppsSum / (d.sessions || 1);
const topRes = Object.entries(d.resolutions).sort(([, a], [, b]) => b - a).slice(0, 5)
.map(([r, n]) => `${r}: ${n}`).join(", ");
return `**${cat.toUpperCase()}**\n Users: ${d.users} | Sessions: ${d.sessions}\n Engagement: ${(avgEng * 100).toFixed(0)}% | Duration: ${avgDur.toFixed(0)}s | Pages/session: ${avgPps.toFixed(1)}\n Conv: ${d.conv} (${(d.conv / (d.users || 1) * 100).toFixed(2)}%)\n Resolutions: ${topRes}`;
}).join("\n\n");
const mave = await fetch("https://app.mavera.io/api/v1/mave/chat", {
method: "POST",
headers: { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" },
body: JSON.stringify({
message: `Recommend creative format adjustments per device:\n\n${deviceBlocks}\n\nFor each device: 1) Ad dimensions/formats 2) Content layout 3) CTA placement 4) Landing page design 5) Content format priorities 6) Do's/don'ts.\n\nAlso: cross-device consistency, adaptation points, mobile-first vs desktop-first recommendation.`,
}),
}).then((r) => r.json());
console.log("--- Device Creative Recommendations ---");
console.log((mave.content || "").slice(0, 3000));
Example Output
--- Device-Specific Creative Recommendations ---
## Mobile (62% of users, 1.8% conv rate)
- **Ad formats:** 9:16 vertical video (15s max), story-format carousels, single image 1080×1080
- **Content layout:** Snackable — one idea per screen, bullet points over paragraphs. Your 45s avg session means they decide in the first scroll.
- **CTA placement:** Fixed bottom bar or within first viewport. With 2.1 pages/session, they won't scroll far.
- **Landing pages:** Optimize for 390×844 (iPhone 14/15). Single-column, thumb-zone CTAs, collapse feature tables into accordions.
- **Don't:** Use horizontal video, multi-column layouts, or forms with more than 3 fields.
## Desktop (31% of users, 3.4% conv rate)
- **Ad formats:** 16:9 landscape video (30-60s), comparison infographics, multi-panel carousel
- **Content layout:** Long-form works here — 3.2 min avg session and 4.8 pages/session means they're evaluating deeply. Include detailed feature tables, customer quotes, and embedded demos.
- **CTA placement:** After value demonstration (not above the fold). Desktop users scroll.
- **Landing pages:** Optimize for 1920×1080. Two-column layouts with sticky nav. Include pricing calculator or interactive demo.
## Cross-Device
- **Keep consistent:** Value proposition, brand colors, core messaging hierarchy
- **Adapt:** CTA text (mobile: "Try Free" / desktop: "Start Your 14-Day Free Trial"), content depth, form length
- **Recommendation:** Mobile-first design, desktop-enhanced. 62% of traffic is mobile, but desktop converts at nearly 2x — invest in both, but design mobile first.
Error Handling
Screen resolution cardinality
Screen resolution cardinality
Hundreds of unique resolutions exist. The code aggregates by device category first, then lists top 5 resolutions per category. Group similar resolutions (e.g. 1920×1080 and 1920×1200 as “Full HD”) for cleaner analysis.
Tablet traffic declining
Tablet traffic declining
If tablet traffic is under 5% of total, consider merging tablet data with desktop for persona purposes. Modern tablets render desktop-class pages.
Smart TV / other devices
Smart TV / other devices
GA4 may report
smart tv or other device categories with minimal traffic. Filter these out unless you specifically target living-room experiences.What’s Next
GA4 Integration
Back to GA4 integration overview
Acquisition Channel × Persona Mapping
Map channel-demographic pairs to personas
Audience Demographics → Persona Creation
Create personas from GA4 demographic data
Mave Agent
Full reference for POST /api/v1/mave/chat