Scenario
Your user profiles tell you who people are. Your event stream tells you what they do. You export raw events from Mixpanel, identify power-user vs. casual usage patterns (feature breadth, session frequency, action depth), and update existing Mavera personas with behavioral data. The result is personas that reflect both demographics and real product behavior.Architecture
Code
import os, requests, time, json
from collections import defaultdict
from datetime import datetime, timedelta
MP_SA = os.environ["MIXPANEL_SERVICE_ACCOUNT"]
MP_SECRET = os.environ["MIXPANEL_SECRET"]
MP_PROJECT = os.environ["MIXPANEL_PROJECT_ID"]
MV = os.environ["MAVERA_API_KEY"]
MB = "https://app.mavera.io/api/v1"
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
today = datetime.now()
from_date = (today - timedelta(days=30)).strftime("%Y-%m-%d")
to_date = today.strftime("%Y-%m-%d")
r = requests.get(
"https://data.mixpanel.com/api/2.0/export",
auth=(MP_SA, MP_SECRET),
params={"project_id": MP_PROJECT, "from_date": from_date, "to_date": to_date},
stream=True,
)
r.raise_for_status()
user_events = defaultdict(lambda: {"events": [], "features": set(), "days": set(), "count": 0})
for line in r.iter_lines():
if not line:
continue
event = json.loads(line)
props = event.get("properties", {})
uid = props.get("distinct_id")
if not uid:
continue
user_events[uid]["events"].append(event.get("event", ""))
user_events[uid]["features"].add(event.get("event", ""))
user_events[uid]["count"] += 1
ts = props.get("time")
if ts:
user_events[uid]["days"].add(datetime.fromtimestamp(ts).strftime("%Y-%m-%d"))
print(f"Processed events for {len(user_events)} users")
patterns = {"power_user": [], "regular": [], "casual": []}
for uid, data in user_events.items():
feature_count = len(data["features"])
event_count = data["count"]
active_days = len(data["days"])
profile = {
"uid": uid, "events": event_count, "features": feature_count,
"active_days": active_days, "top_events": data["events"][:5],
"feature_list": list(data["features"])[:10],
}
if feature_count >= 5 and active_days >= 15:
patterns["power_user"].append(profile)
elif feature_count >= 2 and active_days >= 5:
patterns["regular"].append(profile)
else:
patterns["casual"].append(profile)
existing = requests.get(f"{MB}/personas", headers=MH).json()
mp_personas = {
p["name"]: p for p in (existing if isinstance(existing, list) else [])
if "Mixpanel" in p.get("name", "")
}
for pattern_name, users in patterns.items():
if not users:
continue
label = pattern_name.replace("_", " ").title()
persona_name = f"Mixpanel: {label}"
avg_events = sum(u["events"] for u in users) / len(users)
avg_features = sum(u["features"] for u in users) / len(users)
avg_days = sum(u["active_days"] for u in users) / len(users)
from collections import Counter
all_features = Counter(f for u in users for f in u["feature_list"])
top_features = [f for f, _ in all_features.most_common(8)]
payload = {
"name": persona_name,
"description": (
f"{label} segment enriched with event data (30d). "
f"N={len(users)}. Avg events: {avg_events:.0f}, "
f"features used: {avg_features:.1f}, active days: {avg_days:.1f}. "
f"Top features: {', '.join(top_features[:5])}."
),
"psychographic": {
"usage_pattern": pattern_name,
"avg_events_30d": avg_events,
"avg_features_used": avg_features,
"avg_active_days": avg_days,
"top_features": top_features,
},
}
if persona_name in mp_personas:
pid = mp_personas[persona_name]["id"]
requests.patch(f"{MB}/personas/{pid}", headers=MH, json=payload).raise_for_status()
print(f"Updated: {persona_name} ({pid}) — {len(users)} users")
else:
p = requests.post(f"{MB}/personas", headers=MH, json=payload).json()
print(f"Created: {persona_name} ({p['id']}) — {len(users)} users")
time.sleep(0.3)
const MP_SA = process.env.MIXPANEL_SERVICE_ACCOUNT;
const MP_SECRET = process.env.MIXPANEL_SECRET;
const MP_PROJECT = process.env.MIXPANEL_PROJECT_ID;
const MV = process.env.MAVERA_API_KEY;
const MB = "https://app.mavera.io/api/v1";
const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const mpAuth = "Basic " + Buffer.from(`${MP_SA}:${MP_SECRET}`).toString("base64");
const today = new Date();
const from = new Date(today - 30 * 86400000).toISOString().slice(0, 10);
const to = today.toISOString().slice(0, 10);
const exportRes = await fetch(
`https://data.mixpanel.com/api/2.0/export?project_id=${MP_PROJECT}&from_date=${from}&to_date=${to}`,
{ headers: { Authorization: mpAuth } }
).then((r) => r.text());
const userEvents = {};
for (const line of exportRes.split("\n").filter(Boolean)) {
const event = JSON.parse(line);
const uid = event.properties?.distinct_id;
if (!uid) continue;
userEvents[uid] ??= { events: [], features: new Set(), days: new Set(), count: 0 };
userEvents[uid].events.push(event.event || "");
userEvents[uid].features.add(event.event || "");
userEvents[uid].count++;
if (event.properties?.time) {
userEvents[uid].days.add(new Date(event.properties.time * 1000).toISOString().slice(0, 10));
}
}
console.log(`Processed events for ${Object.keys(userEvents).length} users`);
const patterns = { power_user: [], regular: [], casual: [] };
for (const [uid, data] of Object.entries(userEvents)) {
const fc = data.features.size, ad = data.days.size;
const profile = { uid, events: data.count, features: fc, activeDays: ad, featureList: [...data.features].slice(0, 10) };
if (fc >= 5 && ad >= 15) patterns.power_user.push(profile);
else if (fc >= 2 && ad >= 5) patterns.regular.push(profile);
else patterns.casual.push(profile);
}
const existing = await fetch(`${MB}/personas`, { headers: MH }).then((r) => r.json());
const mpPersonas = Object.fromEntries(
(Array.isArray(existing) ? existing : []).filter((p) => (p.name || "").includes("Mixpanel")).map((p) => [p.name, p])
);
for (const [patternName, users] of Object.entries(patterns)) {
if (!users.length) continue;
const label = patternName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
const name = `Mixpanel: ${label}`;
const avgE = users.reduce((s, u) => s + u.events, 0) / users.length;
const avgF = users.reduce((s, u) => s + u.features, 0) / users.length;
const avgD = users.reduce((s, u) => s + u.activeDays, 0) / users.length;
const featureCount = {};
users.forEach((u) => u.featureList.forEach((f) => { featureCount[f] = (featureCount[f] || 0) + 1; }));
const topFeatures = Object.entries(featureCount).sort(([, a], [, b]) => b - a).slice(0, 8).map(([f]) => f);
const payload = {
name, description: `${label} (${users.length} users, 30d). Avg events: ${avgE.toFixed(0)}, features: ${avgF.toFixed(1)}, days: ${avgD.toFixed(1)}. Top: ${topFeatures.slice(0, 5).join(", ")}.`,
psychographic: { usage_pattern: patternName, avg_events_30d: avgE, top_features: topFeatures },
};
if (mpPersonas[name]) {
await fetch(`${MB}/personas/${mpPersonas[name].id}`, { method: "PATCH", headers: MH, body: JSON.stringify(payload) });
console.log(`Updated: ${name} (${mpPersonas[name].id})`);
} else {
const p = await fetch(`${MB}/personas`, { method: "POST", headers: MH, body: JSON.stringify(payload) }).then((r) => r.json());
console.log(`Created: ${name} (${p.id})`);
}
await new Promise((r) => setTimeout(r, 300));
}
Example Output
Processed events for 8,420 users
Updated: Mixpanel: Power User (per_mp_power_1) — 489 users
Avg events: 1,342 | Features: 7.2 | Days: 22.4
Top: Dashboard View, API Call, Report Create, Alert Set, Export Data
Updated: Mixpanel: Regular (per_mp_reg_2) — 2,891 users
Avg events: 186 | Features: 3.1 | Days: 9.8
Top: Dashboard View, Report Create, Settings Visit
Created: Mixpanel: Casual (per_mp_cas_3) — 5,040 users
Avg events: 12 | Features: 1.4 | Days: 2.1
Top: Dashboard View, Login
Error Handling
Export API returns JSONL
Export API returns JSONL
The Export API returns newline-delimited JSON (one event per line), not a JSON array. The code parses line by line. Large exports (millions of events) should use streaming and process in chunks.
Export volume limits
Export volume limits
The Export API can return millions of events. For properties with high volume, filter by specific events using the
event parameter, or use shorter date ranges.PATCH vs POST for persona updates
PATCH vs POST for persona updates
If Mavera doesn’t support PATCH, use DELETE + POST to replace personas. The code checks for existing personas by name to decide whether to update or create.
What’s Next
Mixpanel Integration
Back to Mixpanel integration overview
Cohort Analysis → Content Strategy
Retention strategies from cohort behavior
Feature Adoption → Messaging
Feature awareness campaigns from adoption data
Personas API
Full reference for POST /api/v1/personas