Skip to content

Archety Amplitude Tracking Sheet

Table of Contents

  1. Tracking Strategy & Principles
  2. Identity Model
  3. User Properties
  4. Event Taxonomy
  5. Implementation Map

Tracking Strategy & Principles

Core Principles

  1. No PII / Content: Never send raw message text, email subjects, or personal content. Only counts, types, lengths, and high-level labels.
  2. Flat, Boring Names: Use consistent snake_case like user_signed_up, message_sent, superpower_triggered.
  3. One Event Per Action: Avoid mega-events. Keep LLM usage separate from messaging.
  4. Consistent Properties: Always include persona_id, channel, is_group_thread where applicable.

Data Privacy

  • Message content: ❌ Never tracked
  • Message length: ✅ Tracked as integer
  • Calendar event titles: ❌ Never tracked
  • Calendar event count: ✅ Tracked
  • Email subjects: ❌ Never tracked
  • Email sender domains: ✅ Tracked (anonymized)

Identity Model

User Identification

amplitude.setUserId(user.id)  // UUID from database

Properties: - user_id: Internal UUID (primary identifier) - phone: Hashed phone number (for correlation only, not PII) - device_id: Automatically handled by Amplitude SDK

Channel Identity

Track as event properties, not separate user IDs: - primary_channel: imessage | telegram | web | ios_app | android_app - channel_source: imessage | telegram_webhook | edge_agent | backend

Groups (Future)

  • group_type=thread, group_id=chat_guid for thread-level analytics
  • group_type=org, group_id=org_123 for B2B (when launched)

User Properties

Track via amplitude.identify() calls - updated on changes, not every event.

Property Type Description Update Trigger
signup_date ISO date First message timestamp On user creation
primary_persona string sage | vex | echo On first message
primary_channel string imessage | telegram On first message
relationship_stage string stranger | acquaintance | friend | best_friend On relationship update
trust_score int (0-100) Relationship trust level On relationship update
rapport_score int (0-100) Relationship rapport level On relationship update
has_group_threads boolean Sage in ≥1 group chat On first group message
superpowers_enabled_count int # of enabled superpowers On enable/disable
calendar_connected boolean Google Calendar OAuth On OAuth grant/revoke
gmail_connected boolean Gmail OAuth On OAuth grant/revoke
total_messages_sent int Lifetime user→Sage Daily batch update
total_messages_received int Lifetime Sage→user Daily batch update
total_llm_tokens_used int Lifetime token count Daily batch update
total_memories int Total memories in mem0 Daily batch update
last_active_date ISO date Most recent message On each message

Future (when billing launches): - plan_tier: free | friend | best_friend | enterprise - billing_status: active | trialing | canceled | past_due - lifetime_revenue_usd: Sum of invoices


Event Taxonomy

1. Acquisition & Onboarding

1.1 user_discovered

When: First message from unknown phone number Implementation: app/main.py::handle_orchestrator_message() - on User creation

{
  "event": "user_discovered",
  "properties": {
    "channel": "imessage" | "telegram",
    "persona_id": "sage" | "echo",  # Inferred from context
    "mode": "direct" | "group",
    "has_participants": true  # If group
  }
}

1.2 phone_verification_completed

When: User completes first bidirectional conversation Implementation: app/orchestrator/relationship_service.py - on stage transition from null→stranger

{
  "event": "phone_verification_completed",
  "properties": {
    "channel": "imessage" | "telegram",
    "persona_id": "sage" | "echo"
  }
}

1.3 persona_selected

When: First interaction with a specific persona Implementation: app/main.py::handle_orchestrator_message() - on first message per persona

{
  "event": "persona_selected",
  "properties": {
    "persona_id": "sage" | "vex" | "echo",
    "channel": "imessage" | "telegram",
    "is_primary": true  # First persona for this user
  }
}

2. Messaging & Engagement

2.1 message_sent_user

When: Every inbound message from user Implementation: app/main.py::handle_orchestrator_message() - after dedup check

{
  "event": "message_sent_user",
  "properties": {
    "channel": "imessage" | "telegram",
    "persona_id": "sage" | "echo",
    "thread_id": "hashed_chat_guid",  # SHA-256 hash for privacy
    "is_group_thread": true | false,
    "message_length_chars": 142,
    "message_type": "text" | "photo" | "voice",
    "group_size": 5,  # Only if is_group_thread=true
    "contains_mention": true,  # @sage mentioned
    "detected_intent": "casual_chat" | "calendar_query" | "email_check" | "task_request",
    "workflow_triggered": "calendar_stress" | null
  }
}

2.2 message_sent_companion

When: Every outbound Sage/Echo message Implementation: - app/orchestrator/two_stage_handler.py::handle() - after response generation - app/messaging/proactive_sender.py::send_to_user() - for proactive messages

{
  "event": "message_sent_companion",
  "properties": {
    "channel": "imessage" | "telegram",
    "persona_id": "sage" | "echo",
    "thread_id": "hashed_chat_guid",
    "is_group_thread": true | false,
    "message_length_chars": 87,
    "message_category": "reply" | "reflex" | "burst" | "proactive_nudge" | "workflow_result",
    "is_reflex": true,  # From reflex library
    "is_burst": true,  # Follow-up bubble
    "burst_position": 2,  # Position in burst (1, 2, 3...)
    "response_latency_ms": 1847,  # Time from user message to this response
    "used_memories": true,  # Referenced mem0 memories
    "memory_count": 3,  # # of memories used
    "memory_types": ["emotional", "factual"],  # Memory categories used
    "relationship_stage": "friend"
  }
}

2.3 conversation_started

When: First message after ≥30min inactivity Implementation: app/orchestrator/conversation_history_service.py - check last message timestamp

{
  "event": "conversation_started",
  "properties": {
    "channel": "imessage" | "telegram",
    "persona_id": "sage" | "echo",
    "thread_id": "hashed_chat_guid",
    "is_group_thread": true | false,
    "hours_since_last": 12.5,
    "time_of_day": "morning" | "afternoon" | "evening" | "night",  # Bucketed
    "day_of_week": "monday",
    "initiated_by": "user" | "companion"  # Who sent first message
  }
}

2.4 conversation_ended

When: No activity for 30min (detected on next message or batch job) Implementation: Background job or lazy evaluation on next message

{
  "event": "conversation_ended",
  "properties": {
    "channel": "imessage" | "telegram",
    "persona_id": "sage" | "echo",
    "thread_id": "hashed_chat_guid",
    "is_group_thread": true | false,
    "duration_sec": 1247,
    "user_messages_count": 8,
    "companion_messages_count": 12,
    "workflow_executions": 2,  # # of workflows during session
    "memories_created": 3,  # New memories added
    "relationship_stage_start": "friend",
    "relationship_stage_end": "friend"  # Changed if progression occurred
  }
}

2.5 group_thread_joined

When: Sage first added to group chat Implementation: app/main.py::handle_orchestrator_message() - on first group message detection

{
  "event": "group_thread_joined",
  "properties": {
    "channel": "imessage" | "telegram",
    "thread_id": "hashed_chat_guid",
    "group_size": 5,  # Estimate from participants array
    "persona_id": "echo"  # Usually Echo for groups
  }
}

2.6 group_plan_summarized

When: Echo posts plan summary in group ("Plan is 7pm at Dante...") Implementation: app/superpowers/catalog/on_demand/group_coordination.py - when plan extracted

{
  "event": "group_plan_summarized",
  "properties": {
    "channel": "imessage" | "telegram",
    "thread_id": "hashed_chat_guid",
    "persona_id": "echo",
    "group_size": 5,
    "plan_type": "dinner" | "trip" | "party" | "meeting" | "other",  # Inferred
    "has_time": true,
    "has_location": true,
    "participant_count": 5
  }
}

2.7 reminder_scheduled

When: User or backend schedules future reminder Implementation: app/models/database.py::ScheduledMessage insert

{
  "event": "reminder_scheduled",
  "properties": {
    "channel": "imessage" | "telegram",
    "persona_id": "sage",
    "thread_id": "hashed_chat_guid",
    "reminder_type": "generic" | "flight_departure" | "call_parents" | "deadline",
    "send_at_timestamp": "2025-11-10T15:00:00Z",
    "hours_until_send": 24.5,
    "created_by": "user_request" | "workflow"  # Manual vs automated
  }
}

2.8 reminder_sent

When: Scheduled reminder delivered Implementation: Scheduler job that processes ScheduledMessage table

{
  "event": "reminder_sent",
  "properties": {
    "channel": "imessage" | "telegram",
    "persona_id": "sage",
    "thread_id": "hashed_chat_guid",
    "reminder_type": "generic" | "flight_departure",
    "scheduled_at": "2025-11-10T15:00:00Z",
    "sent_at": "2025-11-10T15:00:23Z",
    "delay_ms": 23000,  # If delayed
    "created_by": "user_request" | "workflow"
  }
}

3. Superpowers & Integrations

3.1 integration_connected

When: User completes OAuth flow Implementation: app/oauth/routes.py::auth_callback() - after token storage

{
  "event": "integration_connected",
  "properties": {
    "integration_type": "gmail" | "google_calendar",
    "persona_id": "sage",  # Who they were talking to when auth initiated
    "channel": "imessage" | "telegram",
    "trigger_workflow": "calendar_stress"  # Which workflow prompted OAuth
  }
}

3.2 integration_disconnected

When: User revokes OAuth or token expired Implementation: app/oauth/routes.py::revoke_access() or token expiry detection

{
  "event": "integration_disconnected",
  "properties": {
    "integration_type": "gmail" | "google_calendar",
    "reason": "user_revoked" | "token_expired" | "permission_denied",
    "days_connected": 45  # How long it was active
  }
}

3.3 superpower_enabled

When: User turns on superpower (currently all enabled by default, but track when feature launches) Implementation: app/superpowers/manager.py::enable_superpower()

{
  "event": "superpower_enabled",
  "properties": {
    "superpower_id": "travel_brain" | "calendar_stress_shield" | "email_urgency_scan",
    "superpower_category": "travel" | "productivity" | "social" | "money",
    "requires_oauth": true,
    "persona_id": "sage",
    "channel": "imessage" | "telegram",
    "has_required_oauth": true  # Whether user already has needed OAuth
  }
}

3.4 superpower_disabled

When: User turns off superpower Implementation: app/superpowers/manager.py::disable_superpower()

{
  "event": "superpower_disabled",
  "properties": {
    "superpower_id": "travel_brain",
    "superpower_category": "travel",
    "persona_id": "sage",
    "channel": "imessage" | "telegram",
    "days_enabled": 30,  # How long it was on
    "usage_count": 12  # How many times it triggered while enabled
  }
}

3.5 superpower_triggered

When: Backend decides to run superpower for user Implementation: - app/scheduler/background_service.py - for proactive workflows - app/messaging/workflow_detector.py::detect_workflow() - for on-demand

{
  "event": "superpower_triggered",
  "properties": {
    "superpower_id": "calendar_stress_shield",
    "superpower_category": "productivity",
    "trigger_source": "scheduled" | "user_request" | "event_based",
    "trigger_time": "07:30",  # For scheduled
    "channel": "telegram",
    "persona_id": "sage",
    "has_required_oauth": true,
    "workflow_id": "proactive_morning_calendar"
  }
}

3.6 superpower_result_sent

When: Sage sends superpower outcome to user Implementation: app/superpowers/engine.py::execute_workflow() - on completion

{
  "event": "superpower_result_sent",
  "properties": {
    "superpower_id": "calendar_stress_shield",
    "superpower_category": "productivity",
    "channel": "telegram",
    "persona_id": "sage",
    "result_type": "suggestion_only" | "reminder_scheduled" | "data_summary" | "no_action_needed",
    "execution_duration_ms": 3421,
    "data_items_found": 5,  # e.g., # of calendar events
    "action_suggested": true,
    "workflow_id": "proactive_morning_calendar"
  }
}

3.7 superpower_accepted

When: User agrees to suggested action Implementation: app/superpowers/engine.py - when user responds positively to paused workflow

{
  "event": "superpower_accepted",
  "properties": {
    "superpower_id": "calendar_stress_shield",
    "superpower_category": "productivity",
    "action_type": "block_calendar" | "send_draft" | "schedule_reminder" | "other",
    "channel": "telegram",
    "persona_id": "sage",
    "response_time_sec": 120  # How long user took to respond
  }
}

3.8 superpower_rejected

When: User says no or ignores Implementation: app/superpowers/engine.py - negative response or timeout

{
  "event": "superpower_rejected",
  "properties": {
    "superpower_id": "calendar_stress_shield",
    "superpower_category": "productivity",
    "rejection_type": "explicit_no" | "ignored" | "timeout",
    "channel": "telegram",
    "persona_id": "sage",
    "response_time_sec": null  # null if ignored
  }
}

4. LLM Usage & Costs

4.1 llm_request_started

When: Right before LLM API call (optional - useful for latency tracking) Implementation: app/utils/llm_client.py::generate() - start of method

{
  "event": "llm_request_started",
  "properties": {
    "request_id": "uuid-v4",
    "persona_id": "sage",
    "user_id": "user-uuid",  # Set as user property
    "superpower_id": "calendar_stress_shield" | null,
    "prompt_type": "chat_reply" | "reflex_classification" | "workflow_analysis" | "memory_extraction",
    "model": "gpt-4o" | "gpt-4o-mini" | "claude-3-5-sonnet",
    "stage": "stage_1" | "stage_2" | "single",  # Two-stage handler context
    "channel": "imessage" | "telegram"
  }
}

4.2 llm_request_completed

When: After LLM response received Implementation: app/utils/llm_client.py::generate() - after response

{
  "event": "llm_request_completed",
  "properties": {
    "request_id": "uuid-v4",  # Match to started event
    "user_id": "user-uuid",
    "persona_id": "sage",
    "superpower_id": "calendar_stress_shield" | null,
    "prompt_type": "chat_reply",
    "model": "gpt-4o",
    "stage": "stage_2",
    "channel": "telegram",

    # Token metrics
    "prompt_tokens": 1247,
    "completion_tokens": 156,
    "total_tokens": 1403,

    # Cost estimate (calculated client-side)
    "llm_cost_usd": 0.0084,  # Based on model pricing

    # Performance
    "latency_ms": 1847,
    "status": "success" | "error",
    "error_type": "rate_limit" | "timeout" | "invalid_request" | null,

    # Context
    "conversation_history_messages": 10,
    "memories_included": 3,
    "workflow_context_included": true
  }
}

4.3 llm_cost_summary (Batch Daily Event)

When: Daily aggregation of LLM costs per user Implementation: Background job that queries daily totals

{
  "event": "llm_cost_summary",
  "properties": {
    "date": "2025-11-06",
    "total_requests": 42,
    "total_tokens": 58420,
    "total_cost_usd": 2.47,
    "cost_by_model": {
      "gpt-4o": 2.13,
      "gpt-4o-mini": 0.34
    },
    "cost_by_category": {
      "chat_reply": 1.85,
      "workflow": 0.52,
      "memory_extraction": 0.10
    },
    "avg_cost_per_message": 0.059
  }
}

5. Memory System

5.1 memory_created

When: New memory stored in mem0 Implementation: app/memory/mem0_service.py::add_memory() - after successful storage

{
  "event": "memory_created",
  "properties": {
    "persona_id": "sage",
    "channel": "imessage",
    "memory_category": "emotional" | "factual" | "deadline" | "plan" | "preference",
    "namespace_type": "direct" | "group" | "persona",  # Which namespace
    "is_group_memory": false,
    "conversation_context": "casual_chat" | "workflow_result",
    "memory_length_chars": 87,  # Approximate
    "total_memories_for_user": 145  # Running count
  }
}

5.2 memory_retrieved

When: Memories fetched from mem0 for context Implementation: app/memory/mem0_service.py::search_memories() - after retrieval

{
  "event": "memory_retrieved",
  "properties": {
    "persona_id": "sage",
    "channel": "imessage",
    "namespace_type": "direct" | "group",
    "memories_count": 3,
    "memory_categories": ["emotional", "factual"],  # Categories of retrieved memories
    "search_query": "recent_conversation" | "relevant_context" | "specific_topic",
    "retrieval_latency_ms": 234
  }
}

5.3 memory_forgotten

When: User explicitly asks to forget something Implementation: app/memory/boundary_manager.py::handle_forget_command() - on detection

{
  "event": "memory_forgotten",
  "properties": {
    "persona_id": "sage",
    "channel": "imessage",
    "forget_type": "specific" | "general",  # "forget that" vs "forget about my ex"
    "namespace_type": "direct" | "group",
    "memories_deleted": 2  # # of memories removed
  }
}

5.4 boundary_set

When: User sets conversation boundary in group Implementation: app/memory/boundary_manager.py::handle_boundary_command() - on detection

{
  "event": "boundary_set",
  "properties": {
    "persona_id": "echo",
    "channel": "imessage",
    "thread_id": "hashed_chat_guid",
    "boundary_type": "topic_restriction" | "privacy_request",
    "boundary_scope": "this_group"  # Always group context
  }
}

6. Relationship Progression

6.1 relationship_stage_changed

When: User-persona relationship advances Implementation: app/orchestrator/relationship_service.py::update_relationship() - on stage change

{
  "event": "relationship_stage_changed",
  "properties": {
    "persona_id": "sage",
    "channel": "imessage",
    "previous_stage": "stranger",
    "new_stage": "acquaintance",
    "trust_score": 45,
    "rapport_score": 38,
    "days_since_first_message": 3,
    "total_messages_exchanged": 87,
    "inside_jokes_count": 2
  }
}

7. Reliability & Infrastructure

7.1 message_delivery_failed

When: iMessage/Telegram send fails Implementation: Error handling in app/messaging/telegram.py and relay response

{
  "event": "message_delivery_failed",
  "properties": {
    "channel": "imessage" | "telegram",
    "thread_id": "hashed_chat_guid",
    "error_code": "network_timeout" | "invalid_recipient" | "rate_limited",
    "message_category": "reply" | "proactive_nudge",
    "retry_attempt": 1  # If retrying
  }
}

7.2 superpower_error

When: Workflow execution fails Implementation: app/superpowers/engine.py::execute_workflow() - error handler

{
  "event": "superpower_error",
  "properties": {
    "superpower_id": "calendar_stress_shield",
    "superpower_category": "productivity",
    "error_type": "oauth_missing" | "api_error" | "timeout" | "invalid_state",
    "integration_type": "gmail" | "google_calendar" | null,
    "workflow_id": "proactive_morning_calendar",
    "node_id": "fetch_calendar_events"  # Which node failed
  }
}

7.3 oauth_refresh_failed

When: OAuth token refresh fails Implementation: app/oauth/token_manager.py::refresh_token() - on error

{
  "event": "oauth_refresh_failed",
  "properties": {
    "integration_type": "gmail" | "google_calendar",
    "error_code": "invalid_grant" | "network_error",
    "days_since_connected": 45,
    "needs_reauth": true
  }
}

8. Future: Billing & Revenue

Note: Not currently implemented in codebase. Wire these when Stripe integration launches.

8.1 subscription_checkout_started

8.2 subscription_checkout_completed

8.3 subscription_activated

8.4 subscription_canceled

8.5 invoice_paid

8.6 invoice_payment_failed


Implementation Map

Priority 1: Core Foundation (Week 1)

  1. ✅ Set up Amplitude SDK
  2. ✅ Create AnalyticsService wrapper
  3. ✅ Implement user identification & properties
  4. ✅ Wire up messaging events (80% of volume)

Priority 2: Engagement (Week 1-2)

  1. ✅ Implement conversation lifecycle events
  2. ✅ Add group thread tracking
  3. ✅ Track relationship progression

Priority 3: Features (Week 2)

  1. ✅ Superpower events (trigger, result, accept/reject)
  2. ✅ OAuth integration events
  3. ✅ Memory system events

Priority 4: Performance & Cost (Week 2-3)

  1. ✅ LLM usage tracking (token, cost, latency)
  2. ✅ Reliability events (errors, failures)

Priority 5: Future

  1. ⏳ Billing events (when Stripe launches)
  2. ⏳ iOS/Android app events (when apps launch)
  3. ⏳ Web app events (when web interface launches)

Key Metrics & Dashboards

Activation Funnel

user_discovered (100%)
phone_verification_completed (85%)
persona_selected (80%)
First conversation_started (75%)
First superpower_triggered (40%)
First integration_connected (25%)

Engagement Metrics

  • DAU/WAU/MAU: Unique users with message_sent_user per day/week/month
  • Messages per Active User: Avg message_sent_user count per DAU
  • Session Depth: Avg messages per conversation_started event
  • 1:1 vs Group: % of messages with is_group_thread=true

Superpower Adoption

  • Enablement Rate: % users with ≥1 superpower_enabled
  • Trigger Frequency: superpower_triggered per user per day
  • Acceptance Rate: superpower_accepted / superpower_triggered
  • OAuth Conversion: % users completing integration_connected after superpower triggers

LLM Economics

  • Cost per User: Sum(llm_cost_usd) / DAU
  • Cost per Message: Sum(llm_cost_usd) / Count(message_sent_companion)
  • Token Efficiency: Avg total_tokens per prompt_type
  • Model Mix: Distribution of model in llm_request_completed

Retention Cohorts

  • D1/D7/D30 Retention: % users returning after signup
  • Superpower Impact: Retention uplift for users with ≥1 enabled superpower
  • Relationship Impact: Retention by relationship_stage

Revenue (Future)

  • MRR: Sum of subscription_activated monthly charges
  • ARPU: MRR / Active Users
  • LTV:CAC: Lifetime Value : Customer Acquisition Cost
  • Gross Margin: Revenue - LLM Costs per cohort

Privacy & Compliance

Data We NEVER Track

❌ Message text content ❌ Email subjects or bodies ❌ Calendar event titles ❌ Names (except persona names) ❌ Specific locations ❌ OAuth tokens ❌ Phone numbers (only hashed for correlation)

Data We DO Track

✅ Message counts and lengths ✅ Event timestamps ✅ Feature usage (which workflows) ✅ Aggregate metrics (# of calendar events) ✅ Performance data (latency, tokens) ✅ Error types (no sensitive details)

Amplitude Data Retention

  • 30 days: Raw event data
  • 5 years: Aggregated metrics
  • User deletion: Supports GDPR right to be forgotten

Testing & Validation

Manual Testing Checklist

  • Send test message → verify message_sent_user appears in Amplitude
  • Trigger workflow → verify superpower_triggered + superpower_result_sent
  • Complete OAuth → verify integration_connected
  • Send 10 messages → verify total_messages_sent user property updates
  • Advance relationship stage → verify relationship_stage_changed

Automated Tests

  • Unit tests for AnalyticsService methods
  • Integration tests for each event type
  • Mock Amplitude client for CI/CD
  • Load testing (10,000 events/min)

Validation Queries (Amplitude)

-- Daily Active Users
SELECT DATE(timestamp), COUNT(DISTINCT user_id)
FROM events
WHERE event_type = 'message_sent_user'
GROUP BY DATE(timestamp)

-- Superpower Acceptance Rate
SELECT superpower_id,
  COUNT(CASE WHEN event_type = 'superpower_accepted' THEN 1 END) /
  COUNT(CASE WHEN event_type = 'superpower_triggered' THEN 1 END) as acceptance_rate
FROM events
WHERE event_type IN ('superpower_triggered', 'superpower_accepted')
GROUP BY superpower_id

-- LLM Cost per User
SELECT user_id,
  SUM(llm_cost_usd) as total_cost,
  COUNT(*) as total_requests,
  AVG(latency_ms) as avg_latency
FROM events
WHERE event_type = 'llm_request_completed'
GROUP BY user_id

Version History

  • v1.0 (2025-11-06): Initial tracking sheet based on spec + codebase exploration
  • v1.1 (TBD): Add billing events when Stripe launches
  • v1.2 (TBD): Add mobile app events when iOS/Android launch

Document Owner: Engineering Team Last Updated: 2025-11-06 Status: Ready for Implementation ✅