> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# User Activity Timeline → Journey Mapping

> Pull Amplitude user activity timelines for converting and non-converting users and map journey paths with Mave Agent

### 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

```mermaid theme={"dark"}
flowchart LR
A["Amplitude GET /api/2/useractivity"] --> B["Sample converting + non-converting users"] --> C["Aggregate paths and friction points"] --> D["POST /api/v1/mave/chat"] --> E["Journey analysis"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  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])
  ```

  ```javascript JavaScript theme={"dark"}
  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));
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
--- 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

<AccordionGroup>
  <Accordion title="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.</Accordion>
  <Accordion title="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.</Accordion>
  <Accordion title="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.</Accordion>
</AccordionGroup>

***

## What's Next

<CardGroup cols={2}>
  <Card title="Amplitude Integration" icon="wave-square" href="/integrations/amplitude">
    Back to Amplitude integration overview
  </Card>

  <Card title="Behavioral Cohort Focus Group" icon="users-viewfinder" href="/integrations/amplitude/behavioral-cohort-focus-group">
    Compare fast vs slow onboarders in a Focus Group
  </Card>

  <Card title="Mave Agent" icon="brain" href="/api-reference/mave">
    Full reference for POST /api/v1/mave/chat
  </Card>

  <Card title="Personas API" icon="users" href="/api-reference/personas">
    Full reference for POST /api/v1/personas
  </Card>
</CardGroup>
