Scenario
Amplitude’s User Activity endpoint shows exactly what individual users did — every event, in order, with timestamps. You pull activity timelines for a sample of converting and non-converting users, identify common paths to conversion and points where users get stuck, then send the aggregated journey data to Mave for analysis. The result is a journey map grounded in actual behavioral sequences.Architecture
Code
import os, requests, time, json
from collections import Counter, defaultdict
AMP_KEY = os.environ["AMPLITUDE_API_KEY"]
AMP_SECRET = os.environ["AMPLITUDE_SECRET_KEY"]
MV = os.environ["MAVERA_API_KEY"]
amp_auth = (AMP_KEY, AMP_SECRET)
CONVERTER_IDS = os.environ.get("CONVERTER_USER_IDS", "user_101,user_102,user_103,user_104,user_105").split(",")
NON_CONVERTER_IDS = os.environ.get("NON_CONVERTER_USER_IDS", "user_201,user_202,user_203,user_204,user_205").split(",")
def fetch_activity(user_id):
r = requests.get(
"https://amplitude.com/api/2/useractivity",
auth=amp_auth,
params={"user": user_id},
)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 10)))
return fetch_activity(user_id)
if r.status_code != 200:
return []
data = r.json()
return data.get("userData", {}).get("events", [])
def extract_path(events, max_events=30):
path = []
for e in events[:max_events]:
name = e.get("event_type", "unknown")
if name.startswith("$"):
continue
path.append(name)
return path
converter_paths = []
for uid in CONVERTER_IDS:
events = fetch_activity(uid)
path = extract_path(events)
if path:
converter_paths.append(path)
time.sleep(1)
non_converter_paths = []
for uid in NON_CONVERTER_IDS:
events = fetch_activity(uid)
path = extract_path(events)
if path:
non_converter_paths.append(path)
time.sleep(1)
def aggregate_paths(paths):
step_freq = Counter()
bigrams = Counter()
path_lengths = []
last_events = Counter()
for path in paths:
path_lengths.append(len(path))
for event in path:
step_freq[event] += 1
for i in range(len(path) - 1):
bigrams[f"{path[i]} → {path[i+1]}"] += 1
if path:
last_events[path[-1]] += 1
return {
"count": len(paths),
"avg_length": sum(path_lengths) / max(len(path_lengths), 1),
"top_events": step_freq.most_common(10),
"top_transitions": bigrams.most_common(10),
"last_events": last_events.most_common(5),
}
conv_agg = aggregate_paths(converter_paths)
non_conv_agg = aggregate_paths(non_converter_paths)
def format_agg(agg, label):
events = "\n".join(f" {e}: {c} occurrences" for e, c in agg["top_events"])
transitions = "\n".join(f" {t}: {c} times" for t, c in agg["top_transitions"])
last = "\n".join(f" {e}: {c}" for e, c in agg["last_events"])
return (
f" **{label}** ({agg['count']} users, avg {agg['avg_length']:.0f} events/journey)\n"
f" Top events:\n{events}\n"
f" Top transitions:\n{transitions}\n"
f" Final events before exit:\n{last}"
)
journey_data = f"""CONVERTING USERS:
{format_agg(conv_agg, "Converters")}
NON-CONVERTING USERS:
{format_agg(non_conv_agg, "Non-Converters")}"""
mave = requests.post(
"https://app.mavera.io/api/v1/mave/chat",
headers={"Authorization": f"Bearer {MV}", "Content-Type": "application/json"},
json={"message": f"""Analyze these user journey patterns from Amplitude. Common paths to conversion? Where do users get stuck?
{journey_data}
Provide:
1. The "golden path" — the most common event sequence leading to conversion
2. Divergence points — where converters and non-converters split
3. Friction signals — events that appear in non-converter paths but not converter paths
4. Drop-off cliffs — transitions where non-converters commonly exit
5. Recommendations for reducing friction at each identified stuck point
6. A simplified journey map with 4-6 key stages"""},
).json()
print("--- Journey Analysis ---")
print(mave.get("content", "")[:3000])
const AMP_KEY = process.env.AMPLITUDE_API_KEY;
const AMP_SECRET = process.env.AMPLITUDE_SECRET_KEY;
const MV = process.env.MAVERA_API_KEY;
const ampAuth = "Basic " + Buffer.from(`${AMP_KEY}:${AMP_SECRET}`).toString("base64");
const CONVERTER_IDS = (process.env.CONVERTER_USER_IDS || "user_101,user_102,user_103,user_104,user_105").split(",");
const NON_CONVERTER_IDS = (process.env.NON_CONVERTER_USER_IDS || "user_201,user_202,user_203,user_204,user_205").split(",");
async function fetchActivity(userId) {
const res = await fetch(
`https://amplitude.com/api/2/useractivity?user=${encodeURIComponent(userId)}`,
{ headers: { Authorization: ampAuth } }
);
if (res.status === 429) {
const retry = parseInt(res.headers.get("Retry-After") || "10") * 1000;
await new Promise((r) => setTimeout(r, retry));
return fetchActivity(userId);
}
if (!res.ok) return [];
const data = await res.json();
return data.userData?.events || [];
}
function extractPath(events, max = 30) {
return events.slice(0, max)
.map((e) => e.event_type || "unknown")
.filter((n) => !n.startsWith("$"));
}
const converterPaths = [];
for (const uid of CONVERTER_IDS) {
const events = await fetchActivity(uid);
const path = extractPath(events);
if (path.length) converterPaths.push(path);
await new Promise((r) => setTimeout(r, 1000));
}
const nonConverterPaths = [];
for (const uid of NON_CONVERTER_IDS) {
const events = await fetchActivity(uid);
const path = extractPath(events);
if (path.length) nonConverterPaths.push(path);
await new Promise((r) => setTimeout(r, 1000));
}
function aggregatePaths(paths) {
const stepFreq = {}, bigrams = {}, lastEvents = {};
let totalLen = 0;
for (const path of paths) {
totalLen += path.length;
path.forEach((e) => { stepFreq[e] = (stepFreq[e] || 0) + 1; });
for (let i = 0; i < path.length - 1; i++) {
const key = `${path[i]} → ${path[i + 1]}`;
bigrams[key] = (bigrams[key] || 0) + 1;
}
if (path.length) lastEvents[path[path.length - 1]] = (lastEvents[path[path.length - 1]] || 0) + 1;
}
const topN = (obj, n) => Object.entries(obj).sort(([, a], [, b]) => b - a).slice(0, n);
return {
count: paths.length, avgLength: totalLen / (paths.length || 1),
topEvents: topN(stepFreq, 10), topTransitions: topN(bigrams, 10), lastEvents: topN(lastEvents, 5),
};
}
const convAgg = aggregatePaths(converterPaths);
const nonConvAgg = aggregatePaths(nonConverterPaths);
function formatAgg(agg, label) {
const events = agg.topEvents.map(([e, c]) => ` ${e}: ${c}`).join("\n");
const trans = agg.topTransitions.map(([t, c]) => ` ${t}: ${c}`).join("\n");
const last = agg.lastEvents.map(([e, c]) => ` ${e}: ${c}`).join("\n");
return ` **${label}** (${agg.count} users, avg ${agg.avgLength.toFixed(0)} events)\n Top events:\n${events}\n Transitions:\n${trans}\n Last events:\n${last}`;
}
const journeyData = `CONVERTERS:\n${formatAgg(convAgg, "Converters")}\n\nNON-CONVERTERS:\n${formatAgg(nonConvAgg, "Non-Converters")}`;
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: `Analyze journey patterns from Amplitude. Common paths to conversion? Where stuck?\n\n${journeyData}\n\nProvide: 1) Golden path 2) Divergence points 3) Friction signals 4) Drop-off cliffs 5) Friction reduction recommendations 6) Simplified 4-6 stage journey map`,
}),
}).then((r) => r.json());
console.log("--- Journey Analysis ---");
console.log((mave.content || "").slice(0, 3000));
Example Output
--- Journey Analysis ---
## Golden Path (Converting Users)
Sign Up → Complete Profile → Create Project → Invite Teammate → Run First Report → Upgrade
This 6-step sequence appears in 72% of converter journeys. Average
completion time: 3.2 days. The critical transition is Create Project →
Invite Teammate — converters who invite in session 1 convert at 4.1x
the rate of those who wait.
## Divergence Points
| Stage | Converters | Non-Converters |
|-------|-----------|----------------|
| After Sign Up | 89% → Complete Profile | 61% → Dashboard View (browsing) |
| After Create Project | 74% → Invite Teammate | 82% → Settings Visit (fiddling) |
| After First Report | 68% → Upgrade | 45% → Export Data (extracting value without paying) |
## Friction Signals
Events in non-converter paths but rare in converter paths:
- **Help Doc Visit** (3.2x more common) — they're confused
- **Settings Visit** after project creation — looking for something
- **Repeated Dashboard View** without action — passive consumption
## Drop-Off Cliffs
1. Sign Up → (nothing) — 18% of non-converters have 0 events after signup
2. Create Project → Settings — suggests unclear next step
3. Run Report → Export — they got what they needed without paying
## Recommendations
1. Add a "Complete Your Profile" nudge within 30 seconds of signup
2. Auto-prompt team invites immediately after first project creation
3. Gate CSV export behind a trial activation to prevent value extraction
4. Replace help doc links with inline tooltips at friction points
Error Handling
User Activity returns per-user data
User Activity returns per-user data
The
/api/2/useractivity endpoint returns events for a single user ID. You must loop through user IDs, which counts against the 360/hour rate limit. For large samples, batch requests with 1-second delays.Amplitude user IDs
Amplitude user IDs
User IDs in Amplitude can be your internal IDs or Amplitude-generated device IDs. Use the same ID format you pass to
amplitude.identify(). Check in Amplitude → Users → search to verify.System events ($identify, $revenue)
System events ($identify, $revenue)
Amplitude includes system events prefixed with
$. The code filters these out since they represent SDK calls, not user actions. Include $revenue events if you need purchase signals.What’s Next
Amplitude Integration
Back to Amplitude integration overview
Behavioral Cohort Focus Group
Compare fast vs slow onboarders in a Focus Group
Mave Agent
Full reference for POST /api/v1/mave/chat
Personas API
Full reference for POST /api/v1/personas