diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/.posthog-wizard b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/.posthog-wizard new file mode 100644 index 000000000..e69de29bb diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/SKILL.md b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/SKILL.md new file mode 100644 index 000000000..238a02a1d --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/SKILL.md @@ -0,0 +1,60 @@ +--- +name: integration-python +description: PostHog integration for any Python application using the Python SDK +metadata: + author: PostHog + version: dev +--- + +# PostHog integration for Python + +This skill helps you add PostHog analytics to Python applications. + +## Workflow + +Follow these steps in order to complete the integration: + +1. `references/1-begin.md` - PostHog Setup - Begin ← **Start here** +2. `references/2-edit.md` - PostHog Setup - Edit +3. `references/3-revise.md` - PostHog Setup - Revise +4. `references/4-conclude.md` - PostHog Setup - Conclusion + +## Reference files + +- `references/EXAMPLE.md` - Python example project code +- `references/1-begin.md` - Start the event tracking setup process by analyzing the project and creating an event tracking plan +- `references/2-edit.md` - Implement PostHog event tracking in the identified files, following best practices and the example project +- `references/3-revise.md` - Review and fix any errors in the PostHog integration implementation +- `references/4-conclude.md` - Review and fix any errors in the PostHog integration implementation +- `references/python.md` - Python - docs +- `references/posthog-python.md` - PostHog python SDK +- `references/identify-users.md` - Identify users - docs + +The example project shows the target implementation pattern. Consult the documentation for API details. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. +- **Minimal changes**: Add PostHog code alongside existing integrations. Don't replace or restructure existing code. +- **Match the example**: Your implementation should follow the example project's patterns as closely as possible. + +## Framework guidelines + +- Remember that source code is available in the venv/site-packages directory +- posthog is the Python SDK package name +- Install dependencies with `pip install posthog` or `pip install -r requirements.txt` and do NOT use unquoted version specifiers like `>=` directly in shell commands +- In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost +- Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config +- Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions +- NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content +- PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. +- Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit +- The Python SDK has NO identify() method — use posthog_client.set(distinct_id=user_id, properties={...}) to set person properties, or use identify_context(user_id) within a context + +## Identifying users + +Identify users during login and signup events. Refer to the example code and documentation for the correct identify pattern for this framework. If both frontend and backend code exist, pass the client-side session and distinct ID using `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` headers to maintain correlation. + +## Error tracking + +Add PostHog error tracking to relevant files, particularly around critical user flows and API boundaries. diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/1-begin.md b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/1-begin.md new file mode 100644 index 000000000..55f0a8326 --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/1-begin.md @@ -0,0 +1,56 @@ +--- +title: PostHog Setup - Begin +description: Start the event tracking setup process by analyzing the project and creating an event tracking plan +--- + +We're making an event tracking plan for this project. + +This is the first of several phases — plan the events, implement them, revise and validate changes, then conclude by creating a dashboard and writing a setup report. + +## Task list + +As soon as you've read this description and have a rough sense of the work, make a single **call `TaskCreate` immediately** before reading any reference file or beginning analysis. The user is watching the task pane and shouldn't see it sit empty. + +It's fine if your first list is incomplete or imprecise. Seed it with whatever high-level items you can infer from the overview above, then call `TaskCreate` again (or `TaskUpdate` to refine existing items) every time your understanding sharpens: after a phase reveals work you didn't anticipate, after planning surfaces concrete sub-items, after you hit something new. Use `TaskUpdate` to mark items `in_progress` when you start them and `completed` when you finish. Keeping the list current matters more than getting it right on the first call. + +Keep task titles broad and job-oriented. Describe the purpose or area of work with wording like "Planning event tracking", "Identifying users", "Installing PostHog", "Capturing events", or "Creating dashboards", not the specific files, paths, or symbols involved. Adjust the task names according to the user's project and context. + +Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. + +From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. + +Look for opportunities to track client-side events. + +**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: + + - Payment/checkout completion + - Webhook handlers + - Authentication endpoints + +Do not skip server-side events - they capture actions that cannot be tracked client-side. + +Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add with these exact field names: `event_name` (the event name), `event_description` (one sentence), and `file` (the file path the event goes in). The wizard reads this file to surface the plan in the UI. If events already exist, don't duplicate them; supplement them. + +Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. + +As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. + +## Status + +Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: + +[STATUS] Checking project structure. + +Status to report in this phase: + +- Checking project structure +- Verifying PostHog dependencies +- Generating events based on project + +## Abort statuses + +If and only if the instructions have `[ABORT]` states specified, and you clearly match the conditions for an abort, emit the abort message. Do NOT attempt to exit or halt yourself — the wizard's middleware catches `[ABORT]` and terminates the run for you. + +--- + +**Upon completion, continue with:** [2-edit.md](2-edit.md) \ No newline at end of file diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/2-edit.md b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/2-edit.md new file mode 100644 index 000000000..e5f7ffd16 --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/2-edit.md @@ -0,0 +1,36 @@ +--- +title: PostHog Setup - Edit +description: Implement PostHog event tracking in the identified files, following best practices and the example project +--- + +For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. + +Use environment variables for PostHog keys. Do not hardcode PostHog keys. + +If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. + +For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. + +Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. + +Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. + +It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. + +You should also add PostHog exception capture error tracking to these files where relevant. + +Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. + +Remember the documentation and example project resources you were provided at the beginning. Read them now. + +## Status + +Status to report in this phase: + +- Inserting PostHog capture code +- A status message for each file whose edits you are planning, including a high level summary of changes +- A status message for each file you have edited + +--- + +**Upon completion, continue with:** [3-revise.md](3-revise.md) \ No newline at end of file diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/3-revise.md b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/3-revise.md new file mode 100644 index 000000000..3b07f5069 --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/3-revise.md @@ -0,0 +1,22 @@ +--- +title: PostHog Setup - Revise +description: Review and fix any errors in the PostHog integration implementation +--- + +Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. + +Ensure that any components created were actually used. + +Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. + +## Status + +Status to report in this phase: + +- Finding and correcting errors +- Report details of any errors you fix +- Linting, building and prettying + +--- + +**Upon completion, continue with:** [4-conclude.md](4-conclude.md) \ No newline at end of file diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/4-conclude.md b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/4-conclude.md new file mode 100644 index 000000000..d876d4353 --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/4-conclude.md @@ -0,0 +1,57 @@ +--- +title: PostHog Setup - Conclusion +description: Review and fix any errors in the PostHog integration implementation +--- + +Use the PostHog MCP to create a new dashboard named "Analytics basics (wizard)" based on the events created here. Keep the `(wizard)` tag with that exact casing so anyone browsing PostHog can see the wizard created this dashboard, and so a quick search for `(wizard)` surfaces every wizard-created artifact in one go. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. + +Once the dashboard exists, emit its URL on its own line in your assistant message using this exact marker: `[DASHBOARD_URL] `. The wizard parses this marker from your visible message and surfaces the link in the success summary. Mentioning the URL only in thinking or in prose without the marker means the link is dropped. + +Search for a file called `.posthog-events.json` and read it for available events. + +Do not spawn subagents. + +Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, a list of links for the dashboard and insights created, and a "Verify before merging" checklist (see below). Follow this format: + + +# PostHog post-wizard report + +The wizard has completed a deep integration of your project. [Detailed summary of changes] + +[table of events/descriptions/files] + +## Next steps + +We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: + +[links] + +## Verify before merging + +[checklist] + +### Agent skill + +We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. + + + +For the "Verify before merging" checklist, write GitHub-style checkboxes (`- [ ] ...`) covering what the developer (or their coding agent) still needs to do to take this from "wizard finished" to "merged". Include ONLY the items that actually apply to the integration you just performed — judge each against the code you changed in this run, and drop any that don't fit. Phrase each item as a concrete, checkable action. Candidate items, with the condition for including each: + +- Always: "Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code." +- Always: "Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures." +- If you added environment variables: "Add the exact PostHog env var names you added to `.env.example` and any monorepo/bootstrap scripts so collaborators know what to set." +- If this integration ships a minified production browser bundle (most SPA/SSR web frameworks — e.g. Next.js, Nuxt, SvelteKit, Astro, Vite-based apps): "Wire source-map upload (`posthog-cli sourcemap` or your bundler's upload step) into CI so production stack traces de-minify." +- If LLM analytics was set up in this run: "Trigger the LLM call path(s) you instrumented and confirm `$ai_generation` events appear in PostHog AI Observability." +- If the app has user auth and an `identify` call was added: "Confirm the returning-visitor path also calls `identify` — a handler that only identifies on fresh login can leave returning sessions on anonymous distinct IDs." + +Do not invent items beyond what applies. If only the two "Always" items apply, the checklist is just those two. + +Upon completion, remove .posthog-events.json. + +## Status + +Status to report in this phase: + +- Configured dashboard: [insert PostHog dashboard URL] +- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/EXAMPLE.md b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/EXAMPLE.md new file mode 100644 index 000000000..fbb194df2 --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/EXAMPLE.md @@ -0,0 +1,481 @@ +# PostHog Python Example Project + +Repository: https://github.com/PostHog/context-mill +Path: example-apps/python + +--- + +## README.md + +# PostHog Python Example - CLI Todo App + +A simple command-line todo application built with plain Python (no frameworks) demonstrating PostHog integration for CLIs, scripts, data pipelines, and non-web Python applications. + +## Purpose + +This example serves as: +- **Verification** that the context-mill wizard works for plain Python projects +- **Reference implementation** of PostHog best practices for non-framework Python code +- **Working example** you can run and modify + +## Features Demonstrated + +- **Instance-based API** - Uses `Posthog(...)` class instead of module-level API +- **Exception autocapture** - Automatic tracking of unhandled exceptions +- **Proper shutdown** - Uses `shutdown()` to flush events before exit +- **Event tracking** - Captures user actions with `distinct_id` and properties +- **User identification** - Sets properties on users via `identify()`, and updates them later with `set()` and `setOnce()` +- **Error handling** - Manual exception capture for handled errors + +## Quick Start + +### 1. Install Dependencies + +```bash +# Create virtual environment (recommended) +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt +``` + +### 2. Configure PostHog + +```bash +# Copy environment template +cp .env.example .env + +# Edit .env and add your PostHog project token +# POSTHOG_PROJECT_TOKEN=phc_your_project_token_here +# POSTHOG_HOST=https://us.i.posthog.com +``` + +### 3. Run the App + +```bash +# Add a todo +python todo.py add "Buy groceries" + +# List all todos +python todo.py list + +# Complete a todo +python todo.py complete 1 + +# Delete a todo +python todo.py delete 1 + +# Show statistics +python todo.py stats +``` + +## What Gets Tracked + +The app tracks these events in PostHog: + +| Event | Properties | Purpose | +|-------|-----------|---------| +| `todo_added` | `todo_id`, `todo_length`, `total_todos` | When user adds a new todo | +| `todos_viewed` | `total_todos`, `completed_todos` | When user lists todos | +| `todo_completed` | `todo_id`, `time_to_complete_hours` | When user completes a todo | +| `todo_deleted` | `todo_id`, `was_completed` | When user deletes a todo | +| `stats_viewed` | `total_todos`, `completed_todos`, `pending_todos` | When user views stats | + +## Code Structure + +``` +basics/python/ +├── todo.py # Main CLI application +├── requirements.txt # Python dependencies +├── .env.example # Environment variable template +├── .gitignore # Git ignore rules +└── README.md # This file +``` + +## Key Implementation Patterns + +### 1. Instance-Based Initialization + +```python +from posthog import Posthog + +posthog = Posthog( + api_key, + host='https://us.i.posthog.com', + enable_exception_autocapture=True # Automatically capture exceptions +) +``` + +### 2. Event Tracking Pattern + +```python +# Track events with distinct_id +posthog_client.capture( + distinct_id="user_123", + event="event_name", + properties={"key": "value"} +) +``` + +### 3. Proper Shutdown + +```python +try: + # Your application code + pass +finally: + # Always call shutdown() to flush events and close connections + posthog.shutdown() +``` + +### 4. Identifying Users + +```python +# Set person properties on a user profile +posthog_client.set( + distinct_id="user_123", + properties={"email": "user@example.com", "plan": "pro"} +) +``` + +### 5. Exception Handling + +```python +try: + # Code that might fail + risky_operation() +except Exception as e: + # Manually capture handled errors you want to track + posthog_client.capture_exception(e, distinct_id="user_123") +``` + +## Running Without PostHog + +The app works fine without PostHog configured - it simply won't track analytics. You'll see a warning message but the app continues to function normally. + +## Next Steps + +- Modify `todo.py` to experiment with PostHog tracking +- Add new commands and track their usage +- Explore feature flags: `posthog.feature_enabled('flag-name', user_id)` +- Check your PostHog dashboard to see tracked events + +## Learn More + +- [PostHog Python SDK Documentation](https://posthog.com/docs/libraries/python) +- [PostHog Python SDK API Reference](https://posthog.com/docs/references/posthog-python) +- [PostHog Product Analytics](https://posthog.com/docs/product-analytics) + +--- + +## .env.example + +```example +# PostHog Configuration +POSTHOG_PROJECT_TOKEN=phc_your_project_token_here +POSTHOG_HOST=https://us.i.posthog.com + +# Optional: Enable debug mode to see PostHog requests +# POSTHOG_DEBUG=true + +``` + +--- + +## requirements.txt + +```txt +posthog>=3.0.0 +python-dotenv>=1.0.0 + +``` + +--- + +## todo.py + +```py +#!/usr/bin/env python3 +"""Simple CLI Todo App with PostHog Analytics + +A minimal plain Python CLI application demonstrating PostHog integration +for non-framework Python projects (CLIs, scripts, data pipelines, etc.). +""" + +import argparse +import json +import os +import sys +from datetime import datetime +from pathlib import Path +from dotenv import load_dotenv +from posthog import Posthog + +# Load environment variables +load_dotenv() + +# Data file location +DATA_FILE = Path.home() / ".todo_app.json" + + +def initialize_posthog(): + """Initialize PostHog with instance-based API. + + Returns PostHog instance or None if project token not configured. + """ + project_token = os.getenv('POSTHOG_PROJECT_TOKEN') + + if not project_token: + print("WARNING: PostHog not configured (POSTHOG_PROJECT_TOKEN not set)") + print(" App will work but analytics won't be tracked") + return None + + # Create PostHog instance with opinionated defaults + posthog = Posthog( + project_token, + host=os.getenv('POSTHOG_HOST', 'https://us.i.posthog.com'), + debug=os.getenv('POSTHOG_DEBUG', 'False').lower() == 'true', + enable_exception_autocapture=True # Auto-capture unhandled exceptions + ) + + return posthog + + +def get_user_id(): + """Get or create a user ID for this installation. + + Uses a UUID stored in the data file to represent this user. + In a real app, this would be your actual user ID. + """ + import uuid + + if DATA_FILE.exists(): + data = json.loads(DATA_FILE.read_text()) + if 'user_id' in data: + return data['user_id'] + + # Create new user ID + return f"user_{uuid.uuid4().hex[:8]}" + + +def load_todos(): + """Load todos from disk.""" + if not DATA_FILE.exists(): + return {"user_id": get_user_id(), "todos": []} + + return json.loads(DATA_FILE.read_text()) + + +def save_todos(data): + """Save todos to disk.""" + DATA_FILE.write_text(json.dumps(data, indent=2)) + + +def track_event(posthog, event_name, properties=None): + """Track an event with PostHog. + + Uses the real PostHog Python SDK API. + """ + if not posthog: + return + + posthog.capture( + distinct_id=get_user_id(), + event=event_name, + properties=properties or {} + ) + + +def cmd_add(args, posthog): + """Add a new todo item.""" + data = load_todos() + + todo = { + "id": len(data["todos"]) + 1, + "text": args.text, + "completed": False, + "created_at": datetime.now().isoformat() + } + + data["todos"].append(todo) + save_todos(data) + + print(f"Added todo #{todo['id']}: {todo['text']}") + + # Track the event + track_event(posthog, "todo_added", { + "todo_id": todo["id"], + "todo_length": len(todo["text"]), + "total_todos": len(data["todos"]) + }) + + +def cmd_list(args, posthog): + """List all todos.""" + data = load_todos() + + if not data["todos"]: + print("No todos yet! Add one with: todo add 'Your task'") + return + + print(f"\nYour Todos ({len(data['todos'])} total):\n") + + for todo in data["todos"]: + status = "X" if todo["completed"] else " " + print(f" [{status}] #{todo['id']}: {todo['text']}") + + print() + + # Track the event + track_event(posthog, "todos_viewed", { + "total_todos": len(data["todos"]), + "completed_todos": sum(1 for t in data["todos"] if t["completed"]) + }) + + +def cmd_complete(args, posthog): + """Mark a todo as completed.""" + data = load_todos() + + todo = next((t for t in data["todos"] if t["id"] == args.id), None) + + if not todo: + print(f"ERROR: Todo #{args.id} not found") + return + + if todo["completed"]: + print(f"Todo #{args.id} is already completed") + return + + todo["completed"] = True + todo["completed_at"] = datetime.now().isoformat() + save_todos(data) + + print(f"Completed todo #{todo['id']}: {todo['text']}") + + # Track the event + track_event(posthog, "todo_completed", { + "todo_id": todo["id"], + "time_to_complete_hours": ( + datetime.fromisoformat(todo["completed_at"]) - + datetime.fromisoformat(todo["created_at"]) + ).total_seconds() / 3600 + }) + + +def cmd_delete(args, posthog): + """Delete a todo.""" + data = load_todos() + + todo = next((t for t in data["todos"] if t["id"] == args.id), None) + + if not todo: + print(f"ERROR: Todo #{args.id} not found") + return + + data["todos"].remove(todo) + save_todos(data) + + print(f"Deleted todo #{args.id}") + + # Track the event + track_event(posthog, "todo_deleted", { + "todo_id": todo["id"], + "was_completed": todo["completed"] + }) + + +def cmd_stats(args, posthog): + """Show usage statistics.""" + data = load_todos() + + total = len(data["todos"]) + completed = sum(1 for t in data["todos"] if t["completed"]) + pending = total - completed + + print(f"\nStats:\n") + print(f" Total todos: {total}") + print(f" Completed: {completed}") + print(f" Pending: {pending}") + print(f" Completion rate: {(completed/total*100) if total > 0 else 0:.1f}%") + print() + + # Track the event + track_event(posthog, "stats_viewed", { + "total_todos": total, + "completed_todos": completed, + "pending_todos": pending + }) + + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + description="Simple todo app with PostHog analytics" + ) + + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Add command + add_parser = subparsers.add_parser("add", help="Add a new todo") + add_parser.add_argument("text", help="Todo text") + + # List command + subparsers.add_parser("list", help="List all todos") + + # Complete command + complete_parser = subparsers.add_parser("complete", help="Mark todo as completed") + complete_parser.add_argument("id", type=int, help="Todo ID") + + # Delete command + delete_parser = subparsers.add_parser("delete", help="Delete a todo") + delete_parser.add_argument("id", type=int, help="Todo ID") + + # Stats command + subparsers.add_parser("stats", help="Show statistics") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + # Initialize PostHog + posthog = initialize_posthog() + + try: + # Route to appropriate command + if args.command == "add": + cmd_add(args, posthog) + elif args.command == "list": + cmd_list(args, posthog) + elif args.command == "complete": + cmd_complete(args, posthog) + elif args.command == "delete": + cmd_delete(args, posthog) + elif args.command == "stats": + cmd_stats(args, posthog) + + except Exception as e: + print(f"ERROR: {e}") + + # Manually capture handled errors + if posthog: + posthog.capture_exception(e, get_user_id()) + + sys.exit(1) + + finally: + # IMPORTANT: Always shutdown PostHog to flush events + if posthog: + posthog.shutdown() + + +if __name__ == "__main__": + main() + +``` + +--- + diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/identify-users.md b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/identify-users.md new file mode 100644 index 000000000..1417e03a8 --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/identify-users.md @@ -0,0 +1,272 @@ +# Identify users - Docs + +Linking events to specific users enables you to build a full picture of how they're using your product across different sessions, devices, and platforms. + +This is straightforward to do when [capturing backend events](/docs/product-analytics/capture-events?tab=Node.js.md), as you associate events to a specific user using a `distinct_id`, which is a required argument. + +However, in the frontend of a [web](/docs/libraries/js/features.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/features.md#capturing-anonymous-events). + +To link events to specific users, call `identify`: + +PostHog AI + +### Web + +```javascript +posthog.identify( + 'distinct_id', // Replace 'distinct_id' with your user's unique identifier + { email: 'max@hedgehogmail.com', name: 'Max Hedgehog' } // optional: set additional person properties +); +``` + +### Android + +```kotlin +PostHog.identify( + distinctId = distinctID, // Replace 'distinctID' with your user's unique identifier + // optional: set additional person properties + userProperties = mapOf( + "name" to "Max Hedgehog", + "email" to "max@hedgehogmail.com" + ) +) +``` + +### iOS + +```swift +PostHogSDK.shared.identify("distinct_id", // Replace "distinct_id" with your user's unique identifier + userProperties: ["name": "Max Hedgehog", "email": "max@hedgehogmail.com"]) // optional: set additional person properties +``` + +### React Native + +```jsx +posthog.identify('distinct_id', { // Replace "distinct_id" with your user's unique identifier + email: 'max@hedgehogmail.com', // optional: set additional person properties + name: 'Max Hedgehog' +}) +``` + +### Dart + +```dart +await Posthog().identify( + userId: 'distinct_id', // Replace "distinct_id" with your user's unique identifier + userProperties: { + 'email': 'max@hedgehogmail.com', // optional: set additional person properties + 'name': 'Max Hedgehog', + }, +); +``` + +Events captured after calling `identify` are identified events and this creates a person profile if one doesn't exist already. + +Due to the cost of processing them, anonymous events can be up to 4x cheaper than identified events, so it's recommended you only capture identified events when needed. + +## How identify works + +When a user starts browsing your website or app, PostHog automatically assigns them an **anonymous ID**, which is stored locally. + +Provided you've [configured persistence](/docs/libraries/js/persistence.md) to use cookies or `localStorage`, this enables us to track anonymous users – even across different sessions. + +By calling `identify` with a `distinct_id` of your choice (usually the user's ID in your database, or their email), you link the anonymous ID and distinct ID together. + +Thus, all past and future events made with that anonymous ID are now associated with the distinct ID. + +This enables you to do things like associate events with a user from before they log in for the first time, or associate their events across different devices or platforms. + +Using identify in the backend + +Although you can call `identify` using our backend SDKs, it is used most in frontends. This is because there is no concept of anonymous sessions in the backend SDKs, so calling `identify` only updates person profiles. + +## Best practices when using `identify` + +### 1\. Call `identify` as soon as you're able to + +In your frontend, you should call `identify` as soon as you're able to. + +Typically, this is every time your **app loads** for the first time, and directly after your **users log in**. + +This ensures that events sent during your users' sessions are correctly associated with them. + +You only need to call `identify` once per session, and you should avoid calling it multiple times unnecessarily. + +If you call `identify` multiple times with the same data without reloading the page in between, PostHog will ignore the subsequent calls. + +### 2\. Use unique strings for distinct IDs + +If two users have the same distinct ID, their data is merged and they are considered one user in PostHog. Two common ways this can happen are: + +- Your logic for generating IDs does not generate sufficiently strong IDs and you can end up with a clash where 2 users have the same ID. +- There's a bug, typo, or mistake in your code leading to most or all users being identified with generic IDs like `null`, `true`, or `distinctId`. + +PostHog also has built-in protections to stop the most common distinct ID mistakes. + +### 3\. Reset after logout + +If a user logs out on your frontend, you should call `reset()` to unlink any future events made on that device with that user. + +This is important if your users are sharing a computer, as otherwise all of those users are grouped together into a single user due to shared cookies between sessions. + +**We strongly recommend you call `reset` on logout even if you don't expect users to share a computer.** + +You can do that like so: + +PostHog AI + +### Web + +```javascript +posthog.reset() +``` + +### iOS + +```swift +PostHogSDK.shared.reset() +``` + +### Android + +```kotlin +PostHog.reset() +``` + +### React Native + +```jsx +posthog.reset() +``` + +### Dart + +```dart +await Posthog().reset(); +``` + +If you *also* want to reset the `device_id` so that the device will be considered a new device in future events, you can pass `true` as an argument: + +Web + +PostHog AI + +```javascript +posthog.reset(true) +``` + +### 4\. Person profiles and properties + +You'll notice that one of the parameters in the `identify` method is a `properties` object. + +This enables you to set [person properties](/docs/product-analytics/person-properties.md). + +Whenever possible, we recommend passing in all person properties you have available each time you call identify, as this ensures their person profile on PostHog is up to date. + +Person properties can also be set being adding a `$set` property to a event `capture` call. + +See our [person properties docs](/docs/product-analytics/person-properties.md) for more details on how to work with them and best practices. + +### 5\. Use deep links between platforms + +We recommend you call `identify` [as soon as you're able](#1-call-identify-as-soon-as-youre-able), typically when a user signs up or logs in. + +This doesn't work if one or both platforms are unauthenticated. Some examples of such cases are: + +- Onboarding and signup flows before authentication. +- Unauthenticated web pages redirecting to authenticated mobile apps. +- Authenticated web apps prompting an app download. + +In these cases, you can use a [deep link](https://developer.android.com/training/app-links/deep-linking) on Android and [universal links](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) on iOS to identify users. + +1. Use `posthog.get_distinct_id()` to get the current distinct ID. Even if you cannot call identify because the user is unauthenticated, this will return an anonymous distinct ID generated by PostHog. +2. Add the distinct ID to the deep link as query parameters, along with other properties like UTM parameters. +3. When the user is redirected to the app, parse the deep link and handle the following cases: + +- The mobile app is already authenticated. In this case, call [`posthog.alias()`](/docs/libraries/js/features.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. +- The mobile app is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/features.md#identifying-users) with the distinct ID from the web so pre-login mobile events stay connected to the web session. When the user later logs in on mobile, call `identify()` again with your canonical user ID. + +As long as you associate the distinct IDs with `posthog.identify()` or `posthog.alias()`, you can track events generated across platforms. + +Here's an example implementation for handling deep links from web to mobile: + +PostHog AI + +### iOS + +```swift +import PostHog +class DeepLinkIdentityManager { + static let shared = DeepLinkIdentityManager() + // MARK: - Deep Link Received + func handleDeepLink(_ url: URL, isAuthenticatedOnMobile: Bool) { + guard let webDistinctId = URLComponents(url: url, resolvingAgainstBaseURL: true)? + .queryItems?.first(where: { $0.name == "ph_distinct_id" })?.value else { + return + } + if isAuthenticatedOnMobile { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHogSDK.shared.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHogSDK.shared.identify(webDistinctId) + } + } + // MARK: - Login/Signup + func handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHogSDK.shared.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + func handleLogout() { + PostHogSDK.shared.reset() + } +} +``` + +### Android + +```kotlin +import android.net.Uri +import com.posthog.PostHog +object DeepLinkIdentityManager { + // Deep Link Received + fun handleDeepLink(uri: Uri, isAuthenticatedOnMobile: Boolean) { + val webDistinctId = uri.getQueryParameter("ph_distinct_id") ?: return + if (isAuthenticatedOnMobile) { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHog.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHog.identify(webDistinctId) + } + } + // Login/Signup + fun handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHog.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + fun handleLogout() { + PostHog.reset() + } +} +``` + +## Further reading + +- [Identifying users docs](/docs/product-analytics/identify.md) +- [How person processing works](/docs/how-posthog-works/ingestion-pipeline.md#2-person-processing) +- [An introductory guide to identifying users in PostHog](/tutorials/identifying-users-guide.md) + +### Community questions + +Ask a question + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/posthog-python.md b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/posthog-python.md new file mode 100644 index 000000000..e3a1a66bf --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/posthog-python.md @@ -0,0 +1,1672 @@ +# PostHog Python SDK + +**SDK Version:** 7.21.3 + +Integrate PostHog into any python application. + +## Categories + +- Initialization +- Identification +- Capture +- Error Tracking +- Feature flags +- Contexts +- Events +- Client management + +## PostHog + +This is the SDK reference for the PostHog Python SDK. You can learn more about example usage in the [Python SDK documentation](/docs/libraries/python). You can also follow [Flask](/docs/libraries/flask) and [Django](/docs/libraries/django) guides to integrate PostHog into your project. For long-running applications, create one client during application startup and reuse it for the lifetime of the process. This keeps background queues predictable and makes shutdown flushing straightforward. Multiple clients are still supported for intentional multi-project or multi-host setups. + +### Initialization methods + +#### Client() + +**Release Tag:** public + +Initialize a new PostHog client instance. + +### Parameters + +- **`project_api_key?`** (`str`) - PostHog project API key/token. +- **`host`** (`any`) - PostHog host. Defaults to the US ingestion endpoint when not set. App hosts such as ``https://us.posthog.com`` are mapped to the corresponding ingestion host. +- **`debug`** (`bool`) - Enable verbose SDK logging and re-raise errors from public API methods. +- **`max_queue_size`** (`int`) - Maximum number of events buffered before upload. +- **`send`** (`bool`) - If False, queueing succeeds but events are not sent. +- **`on_error`** (`any`) - Optional callback invoked by background consumers when an upload fails. +- **`flush_at`** (`int`) - Number of queued events that triggers a batch upload. +- **`flush_interval`** (`float`) - Maximum seconds a background consumer waits before flushing a partial batch. +- **`gzip`** (`bool`) - Whether to gzip event upload payloads. +- **`max_retries`** (`int`) - Number of upload retries for background consumers. +- **`sync_mode`** (`bool`) - If True, send each event synchronously instead of using background worker threads. +- **`timeout`** (`int`) - HTTP request timeout in seconds for event uploads. +- **`thread`** (`int`) - Number of background consumer threads. +- **`poll_interval`** (`int`) - Seconds between local feature flag definition refreshes. +- **`personal_api_key`** (`any`) - Personal API key used for local feature flag evaluation and remote config payloads. +- **`disabled`** (`bool`) - If True, disable captures and API requests. Useful in tests. +- **`disable_geoip`** (`bool`) - Whether to disable server-side GeoIP enrichment. Defaults to True. +- **`is_server`** (`bool`) - Whether events are emitted from a server-side runtime. Defaults to True; set to False when using the SDK as a client/CLI so the device OS is attributed to the person normally. +- **`historical_migration`** (`bool`) - Mark events as historical migration imports. +- **`feature_flags_request_timeout_seconds`** (`int`) - Timeout in seconds for feature flag and remote config requests. +- **`feature_flags_request_max_retries`** (`int`) - Number of retries for feature flag requests after network, transport, or timeout failures. Defaults to 1. Set to 0 to disable retries. +- **`super_properties`** (`any`) - Properties merged into every captured event. +- **`enable_exception_autocapture`** (`bool`) - Automatically capture uncaught exceptions. +- **`log_captured_exceptions`** (`bool`) - Also log exceptions captured by error tracking. +- **`project_root`** (`any`) - Root path used to determine in-app stack frames for captured exceptions. Defaults to the current working directory. +- **`privacy_mode`** (`bool`) - For AI observability, capture usage metadata without prompt inputs or outputs. +- **`before_send`** (`any`) - Optional callback that can modify or drop events before upload. Return ``None`` to drop an event. +- **`flag_fallback_cache_url`** (`any`) - Optional feature flag fallback cache URL, such as ``memory://local/?ttl=300&size=10000`` or a Redis URL. +- **`enable_local_evaluation`** (`bool`) - Whether to poll feature flag definitions for local evaluation when a personal API key is configured. +- **`flag_definition_cache_provider?`** (`FlagDefinitionCacheProvider`) - Optional external cache provider for sharing feature flag definitions across workers. +- **`capture_exception_code_variables`** (`bool`) - Capture local variable values on exception stack frames. +- **`code_variables_mask_patterns`** (`any`) - Variable-name patterns to mask when capturing code variables. +- **`code_variables_ignore_patterns`** (`any`) - Variable-name patterns to omit when capturing code variables. +- **`code_variables_mask_url_credentials`** (`any`) - Scrub credentials embedded in URLs/DSNs (e.g. ``user:pass@host``) from captured code variables, regardless of the surrounding variable name. Defaults to True. +- **`code_variables_detect_secrets`** (`any`) - Last-resort entropy-based detection that redacts high-entropy secret-looking values (API keys, tokens, strong passwords) sitting in innocuously-named variables, after the name and URL checks. Skips structured ids (UUIDs, ObjectIds, hashes). Defaults to True. +- **`in_app_modules`** (`UnionType[list[str], any]`) - Module/package prefixes treated as in-app frames in captured exceptions. +- **`enable_exception_autocapture_rate_limiting`** (`bool`) - Rate limit autocaptured exceptions client-side with a token bucket per exception type. Disabled by default. +- **`exception_autocapture_bucket_size`** (`int`) - Maximum burst of autocaptured exceptions allowed per exception type (token bucket size, clamped to 0-100). +- **`exception_autocapture_refill_rate`** (`int`) - Tokens restored per refill interval for each exception type's bucket. +- **`exception_autocapture_refill_interval_seconds`** (`int`) - Seconds between token refills for autocaptured exception rate limiting. +- **`_dedicated_ai_endpoint`** (`bool`) + +### Returns + +- `None` + +### Examples + +```python +from posthog import Posthog + +posthog = Posthog('', host='') +``` + +--- + +### Identification methods + +#### alias() + +**Release Tag:** public + +Create an alias between two distinct IDs. + +### Parameters + +- **`previous_id?`** (`str`) - The previous distinct ID. +- **`distinct_id?`** (`str`) - The new distinct ID to alias to. +- **`timestamp`** (`datetime`) - The timestamp of the event. +- **`uuid?`** (`str`) - A unique identifier for the event. If provided, it must be a valid UUID string or uuid.UUID instance; invalid values are ignored and replaced with a newly generated UUID. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this event. + +### Returns + +- `Optional[str]` + +### Examples + +```python +posthog.alias(previous_id='distinct_id', distinct_id='alias_id') +``` + +--- + +#### group_identify() + +**Release Tag:** public + +Identify a group and set its properties. + +### Parameters + +- **`group_type?`** (`str`) - The type of group (e.g., 'company', 'team'). +- **`group_key?`** (`str`) - The unique identifier for the group. +- **`properties?`** (`dict[str, Any]`) - A dictionary of properties to set on the group. +- **`timestamp`** (`datetime`) - The timestamp of the event. +- **`uuid`** (`str`) - A unique identifier for the event. If provided, it must be a valid UUID string or uuid.UUID instance; invalid values are ignored and replaced with a newly generated UUID. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this event. +- **`distinct_id`** (`Number`) - The distinct ID of the user performing the action. + +### Returns + +- `Optional[str]` + +### Examples + +```python +posthog.group_identify('company', 'company_id_in_your_db', { + 'name': 'Awesome Inc.', + 'employees': 11 +}) +``` + +--- + +#### set() + +**Release Tag:** public + +Set properties on a person profile. + +### Parameters + +- **`kwargs?`** (`Unpack[OptionalSetArgs]`) + +### Returns + +- `Optional[str]` + +### Examples + +```python +# Set with distinct id +posthog.set(distinct_id='user123', properties={'name': 'Max Hedgehog'}) +``` + +--- + +#### set_once() + +**Release Tag:** public + +Set properties on a person profile only if they haven't been set before. + +### Parameters + +- **`kwargs?`** (`Unpack[OptionalSetArgs]`) + +### Returns + +- `Optional[str]` + +### Examples + +```python +posthog.set_once(distinct_id='user123', properties={'initial_signup_date': '2024-01-01'}) +``` + +--- + +### Capture methods + +#### capture() + +**Release Tag:** public + +Captures an event manually. [Learn about capture best practices](https://posthog.com/docs/product-analytics/capture-events) + +### Parameters + +- **`event?`** (`str`) - The event name to capture. +- **`kwargs?`** (`Unpack[OptionalCaptureArgs]`) + +### Returns + +- `Optional[str]` + +### Examples + +#### Anonymous event + +```python +# Anonymous event +posthog.capture('some-anon-event') +``` + +#### Context usage + +```python +# Context usage +from posthog import identify_context, new_context +with new_context(): + identify_context('distinct_id_of_the_user') + posthog.capture('user_signed_up') + posthog.capture('user_logged_in') + posthog.capture('some-custom-action', distinct_id='distinct_id_of_the_user') +``` + +#### Set event properties + +```python +# Set event properties +posthog.capture( + "user_signed_up", + distinct_id="distinct_id_of_the_user", + properties={ + "login_type": "email", + "is_free_trial": "true" + } +) +``` + +#### Page view event + +```python +# Page view event +posthog.capture('$pageview', distinct_id="distinct_id_of_the_user", properties={'$current_url': 'https://example.com'}) +``` + +--- + +### Error Tracking methods + +#### capture_exception() + +**Release Tag:** public + +Capture an exception for error tracking. + +### Parameters + +- **`exception?`** (`BaseException`) - The exception to capture. +- **`kwargs?`** (`Unpack[OptionalCaptureArgs]`) + +### Returns + +- `Optional[str]` + +### Examples + +```python +try: + # Some code that might fail + pass +except Exception as e: + posthog.capture_exception(e, 'user_distinct_id', properties=additional_properties) +``` + +--- + +### Feature flags methods + +#### evaluate_flags() + +**Release Tag:** public + +Evaluate all feature flags for a user in a single call and return a :class:`FeatureFlagEvaluations` snapshot. Branch on ``.is_enabled()`` / ``.get_flag()`` and pass the same snapshot to :meth:`capture` via the ``flags`` option so events carry the exact flag values the code branched on. Prefer this over repeated ``get_feature_flag()`` calls and over ``capture(send_feature_flags=True)`` — it consolidates flag evaluation into a single ``/flags`` request per incoming request. Local evaluation is transparent: when the poller resolves a flag, the snapshot's ``$feature_flag_called`` events are tagged ``locally_evaluated=True`` and reason ``"Evaluated locally"``. + +### Parameters + +- **`distinct_id`** (`Number`) - The user's distinct ID. If ``None``, falls back to the context distinct_id. If still unresolvable, returns an empty snapshot. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - Mapping of group type to group key. +- **`person_properties?`** (`dict[str, Any]`) - Person properties to use for evaluation. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - Group properties keyed by group type. +- **`only_evaluate_locally`** (`bool`) - If True, never fall back to remote evaluation — flags that can't be evaluated locally are simply omitted from the snapshot. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup. +- **`flag_keys?`** (`list[str]`) - Optional list of flag keys to scope the underlying ``/flags`` request to a subset. +- **`device_id?`** (`str`) - Optional device ID override. If not provided, falls back to the context device_id (which may be set via tracing headers). Used by experience-continuity flags to match users across distinct_id changes. + +### Returns + +- `FeatureFlagEvaluations` + +### Examples + +```python +flags = posthog.evaluate_flags( + "user_123", + person_properties={"plan": "enterprise"}, +) +if flags.is_enabled("new-dashboard"): + render_new_dashboard() +posthog.capture("page_viewed", distinct_id="user_123", flags=flags) +``` + +--- + +#### feature_enabled() + +**Release Tag:** public + +Check if a feature flag is enabled for a user. + +### Parameters + +- **`key?`** (`str`) - The feature flag key. +- **`distinct_id?`** (`Number`) - The distinct ID of the user. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`only_evaluate_locally`** (`bool`) - Whether to only evaluate locally. +- **`send_feature_flag_events`** (`bool`) - Whether to send feature flag events. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `Optional[bool]` + +### Examples + +```python +is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') +if is_my_flag_enabled: + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') +``` + +--- + +#### feature_flag_definitions() + +**Release Tag:** public + +Return feature flag definitions loaded for local evaluation. Returns: The currently loaded feature flag definitions, or ``None`` before local evaluation has loaded definitions. + +### Returns + +- `None` + +--- + +#### get_all_flags() + +**Release Tag:** public + +Get all feature flags for a user. + +### Parameters + +- **`distinct_id?`** (`Number`) - The distinct ID of the user. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`only_evaluate_locally`** (`bool`) - Whether to only evaluate locally. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`flag_keys_to_evaluate?`** (`list[str]`) - A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `Optional[dict[str, Union[bool, str]]]` + +### Examples + +```python +posthog.get_all_flags('distinct_id_of_your_user') +``` + +--- + +#### get_all_flags_and_payloads() + +**Release Tag:** public + +Get all feature flags and their payloads for a user. + +### Parameters + +- **`distinct_id?`** (`Number`) - The distinct ID of the user. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`only_evaluate_locally`** (`bool`) - Whether to only evaluate locally. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`flag_keys_to_evaluate?`** (`list[str]`) - A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `FlagsAndPayloads` + +### Examples + +```python +posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +``` + +--- + +#### get_feature_flag() + +**Release Tag:** public + +Get multivariate feature flag value for a user. + +### Parameters + +- **`key?`** (`str`) - The feature flag key. +- **`distinct_id?`** (`Number`) - The distinct ID of the user. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`only_evaluate_locally`** (`bool`) - Whether to only evaluate locally. +- **`send_feature_flag_events`** (`bool`) - Whether to send feature flag events. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `Union[bool, str, any]` + +### Examples + +```python +enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') +``` + +--- + +#### get_feature_flag_payload() + +**Release Tag:** public + +Get the payload for a feature flag. + +### Parameters + +- **`key?`** (`str`) - The feature flag key. +- **`distinct_id?`** (`Number`) - The distinct ID of the user. +- **`match_value`** (`bool`) - The specific flag value to get payload for. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`only_evaluate_locally`** (`bool`) - Whether to only evaluate locally. +- **`send_feature_flag_events`** (`bool`) - Deprecated. Use get_feature_flag() instead if you need events. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `Optional[object]` + +### Examples + +```python +is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user') + +if is_my_flag_enabled: + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') +``` + +--- + +#### get_feature_flags_and_payloads() + +**Release Tag:** public + +Get feature flags and payloads for a user. + +### Parameters + +- **`distinct_id?`** (`Number`) - The distinct ID of the user. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`flag_keys_to_evaluate?`** (`list[str]`) - A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `FlagsAndPayloads` + +### Examples + +```python +result = posthog.get_feature_flags_and_payloads('') +``` + +--- + +#### get_feature_payloads() + +**Release Tag:** public + +Get feature flag payloads for a user. + +### Parameters + +- **`distinct_id?`** (`Number`) - The distinct ID of the user. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`flag_keys_to_evaluate?`** (`list[str]`) - A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `dict[str, str]` + +### Examples + +```python +payloads = posthog.get_feature_payloads('') +``` + +--- + +#### get_feature_variants() + +**Release Tag:** public + +Get feature flag variants for a user. + +### Parameters + +- **`distinct_id?`** (`Number`) - The distinct ID of the user. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`flag_keys_to_evaluate?`** (`list[str]`) - A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `dict[str, Union[bool, str]]` + +--- + +#### get_flags_decision() + +**Release Tag:** public + +Get feature flags decision. + +### Parameters + +- **`distinct_id`** (`Number`) - The distinct ID of the user. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`flag_keys_to_evaluate?`** (`list[str]`) - A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `FlagsResponse` + +### Examples + +```python +decision = posthog.get_flags_decision('user123') +``` + +--- + +#### get_remote_config_payload() + +**Release Tag:** public + +Get the payload for a remote config feature flag. + +### Parameters + +- **`key?`** (`str`) - The remote config feature flag key. + +### Returns + +- `None` + +--- + +#### load_feature_flags() + +**Release Tag:** public + +Load feature flags for local evaluation. + +### Returns + +- `None` + +### Examples + +```python +posthog.load_feature_flags() +``` + +--- + +### Other methods + +#### flush() + +**Release Tag:** public + +Force a flush from the internal queue to the server. Do not use directly, call `shutdown()` instead. + +### Parameters + +- **`timeout_seconds?`** (`float`) - Maximum seconds to wait for the queue to flush. Defaults to 10 seconds. Pass ``None`` to wait indefinitely. + +### Returns + +- `any` + +### Examples + +```python +posthog.capture('event_name') +posthog.flush() # Ensures the event is sent immediately +``` + +--- + +#### get_feature_flag_result() + +**Release Tag:** public + +Get a FeatureFlagResult object which contains the flag result and payload for a key by evaluating locally or remotely depending on whether local evaluation is enabled and the flag can be locally evaluated. This also captures the `$feature_flag_called` event unless `send_feature_flag_events` is `False`. + +### Parameters + +- **`key?`** (`str`) - The feature flag key. +- **`distinct_id?`** (`Number`) - The distinct ID of the user. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - A dictionary of group information. +- **`person_properties?`** (`dict[str, Any]`) - A dictionary of person properties. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - A dictionary of group properties. +- **`only_evaluate_locally`** (`bool`) - Whether to only evaluate locally. +- **`send_feature_flag_events`** (`bool`) - Whether to send feature flag events. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP for this request. +- **`device_id?`** (`str`) - The device ID for this request. + +### Returns + +- `Optional[FeatureFlagResult]` + +### Examples + +```python +flag_result = posthog.get_feature_flag_result('flag-key', 'distinct_id_of_your_user') +if flag_result and flag_result.get_value() == 'variant-key': + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flag_result.payload +``` + +--- + +#### join() + +**Release Tag:** public + +End the consumer thread once the queue is empty. Do not use directly, call `shutdown()` instead. + +### Returns + +- `any` + +### Examples + +```python +posthog.join() +``` + +--- + +#### shutdown() + +**Release Tag:** public + +Flush all messages and cleanly shutdown the client. Call this before the process ends in serverless environments to avoid data loss. + +### Returns + +- `any` + +### Examples + +```python +posthog.shutdown() +``` + +--- + +### Contexts methods + +#### get_tags() + +**Release Tag:** public + +Get all tags from the current context. Returns: Dict of all tags in the current context. + +### Returns + +- `dict[str, Any]` + +--- + +#### identify_context() + +**Release Tag:** public + +Identify the current context with a distinct ID. + +### Parameters + +- **`distinct_id?`** (`str`) - The distinct ID to associate with the current context and its children. + +### Returns + +- `any` + +--- + +#### new_context() + +**Release Tag:** public + +Create a new context for managing shared state. Learn more about [contexts](/docs/libraries/python#contexts). + +### Parameters + +- **`fresh`** (`bool`) - Whether to create a fresh context that doesn't inherit from parent. +- **`capture_exceptions?`** (`bool`) - Whether to automatically capture exceptions in this context. If omitted, defaults to this client's exception autocapture setting. + +### Returns + +- `None` + +### Examples + +```python +with client.new_context(): + client.identify_context('') + client.capture('event_name') +``` + +--- + +#### scoped() + +**Release Tag:** public + +Decorator that creates a new context for the wrapped function using this client. + +### Parameters + +- **`fresh`** (`bool`) - Whether to create a fresh context that doesn't inherit from parent. +- **`capture_exceptions?`** (`bool`) - Whether to automatically capture exceptions in this context. If omitted, defaults to this client's exception autocapture setting. + +### Returns + +- `None` + +--- + +#### set_context_device_id() + +**Release Tag:** public + +Set the device ID for the current context. + +### Parameters + +- **`device_id?`** (`str`) - The device ID to associate with the current context and its children. + +### Returns + +- `any` + +--- + +#### set_context_session() + +**Release Tag:** public + +Set the session ID for the current context. + +### Parameters + +- **`session_id?`** (`str`) - The session ID to associate with the current context and its children. + +### Returns + +- `any` + +--- + +#### tag() + +**Release Tag:** public + +Add a tag to the current context. + +### Parameters + +- **`name?`** (`str`) - The tag key. +- **`value?`** (`Any`) - The tag value. + +### Returns + +- `any` + +--- + +## PostHog Module Functions + +Global functions available in the PostHog module + +### Identification methods + +#### alias() + +**Release Tag:** public + +Associate user behaviour before and after they e.g. register, login, or perform some other identifying action. + +**Notes:** + +To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call. This will allow you to answer questions like "Which marketing channels leads to users churning after a month?" or "What do users do on our website before signing up?". Particularly useful for associating user behaviour before and after they e.g. register, login, or perform some other identifying action. + +### Parameters + +- **`previous_id?`** (`str`) - The unique ID of the user before +- **`distinct_id?`** (`str`) - The current unique id +- **`timestamp?`** (`datetime`) - Optional timestamp for the event +- **`uuid?`** (`str`) - Optional UUID for the event +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup + +### Returns + +- `Optional[str]` + +### Examples + +```python +# Alias user +from posthog import alias +alias(previous_id='distinct_id', distinct_id='alias_id') +``` + +--- + +#### group_identify() + +**Release Tag:** public + +Set properties on a group. + +### Parameters + +- **`group_type?`** (`str`) - Type of your group +- **`group_key?`** (`str`) - Unique identifier of the group +- **`properties?`** (`dict[str, Any]`) - Properties to set on the group +- **`timestamp?`** (`datetime`) - Optional timestamp for the event +- **`uuid?`** (`str`) - Optional UUID for the event +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup +- **`distinct_id`** (`Number`) - Optional distinct ID of the user performing the action + +### Returns + +- `Optional[str]` + +### Examples + +```python +# Group identify +from posthog import group_identify +group_identify('company', 'company_id_in_your_db', { + 'name': 'Awesome Inc.', + 'employees': 11 +}) +``` + +--- + +#### identify_context() + +**Release Tag:** public + +Identify the current context with a distinct ID. + +### Parameters + +- **`distinct_id?`** (`str`) - The distinct ID to associate with the current context and its children + +### Returns + +- `None` + +### Examples + +```python +from posthog import identify_context +identify_context("user_123") +``` + +--- + +#### set() + +**Release Tag:** public + +Set properties on a user record. + +**Notes:** + +This will overwrite previous people property values. Generally operates similar to `capture`, with distinct_id being an optional argument, defaulting to the current context's distinct ID. If there is no context-level distinct ID, and no override distinct_id is passed, this function will do nothing. Context tags are folded into $set properties, so tagging the current context and then calling `set` will cause those tags to be set on the user (unlike capture, which causes them to just be set on the event). + +### Parameters + +- **`kwargs?`** (`Unpack[OptionalSetArgs]`) + +### Returns + +- `Optional[str]` + +### Examples + +```python +# Set person properties +from posthog import set +set(distinct_id='distinct_id', properties={'name': 'Max Hedgehog'}) +``` + +--- + +#### set_once() + +**Release Tag:** public + +Set properties on a user record, only if they do not yet exist. + +**Notes:** + +This will not overwrite previous people property values, unlike `set`. Otherwise, operates in an identical manner to `set`. + +### Parameters + +- **`kwargs?`** (`Unpack[OptionalSetArgs]`) + +### Returns + +- `Optional[str]` + +### Examples + +```python +# Set property once +from posthog import set_once +set_once(distinct_id='distinct_id', properties={'initial_url': '/blog'}) +``` + +--- + +### Events methods + +#### capture() + +**Release Tag:** public + +Capture anything a user does within your system. + +**Notes:** + +Capture allows you to capture anything a user does within your system, which you can later use in PostHog to find patterns in usage, work out which features to improve or where people are giving up. A capture call requires an event name to specify the event. We recommend using [verb] [noun], like `movie played` or `movie updated` to easily identify what your events mean later on. Capture takes a number of optional arguments, which are defined by the `OptionalCaptureArgs` type. + +### Parameters + +- **`event?`** (`str`) - The event name to specify the event **kwargs: Optional arguments including: +- **`kwargs?`** (`Unpack[OptionalCaptureArgs]`) + +### Returns + +- `Optional[str]` + +### Examples + +#### Context and capture usage + +```python +# Context and capture usage +from posthog import new_context, identify_context, tag_context, capture +# Enter a new context (e.g. a request/response cycle, an instance of a background job, etc) +with new_context(): + # Associate this context with some user, by distinct_id + identify_context('some user') + + # Capture an event, associated with the context-level distinct ID ('some user') + capture('movie started') + + # Capture an event associated with some other user (overriding the context-level distinct ID) + capture('movie joined', distinct_id='some-other-user') + + # Capture an event with some properties + capture('movie played', properties={'movie_id': '123', 'category': 'romcom'}) + + # Capture an event with some properties + capture('purchase', properties={'product_id': '123', 'category': 'romcom'}) + # Capture an event with some associated group + capture('purchase', groups={'company': 'id:5'}) + + # Adding a tag to the current context will cause it to appear on all subsequent events + tag_context('some-tag', 'some-value') + + capture('another-event') # Will be captured with `'some-tag': 'some-value'` in the properties dict +``` + +#### Set event properties + +```python +# Set event properties +from posthog import capture +capture( + "user_signed_up", + distinct_id="distinct_id_of_the_user", + properties={ + "login_type": "email", + "is_free_trial": "true" + } +) +``` + +--- + +#### capture_exception() + +**Release Tag:** public + +Capture exceptions that happen in your code. + +**Notes:** + +Capture exception is idempotent - if it is called twice with the same exception instance, only a occurrence will be tracked in posthog. This is because, generally, contexts will cause exceptions to be captured automatically. However, to ensure you track an exception, if you catch and do not re-raise it, capturing it manually is recommended, unless you are certain it will have crossed a context boundary (e.g. by existing a `with posthog.new_context():` block already). If the passed exception was raised and caught, the captured stack trace will consist of every frame between where the exception was raised and the point at which it is captured (the "traceback"). If the passed exception was never raised, e.g. if you call `posthog.capture_exception(ValueError("Some Error"))`, the stack trace captured will be the full stack trace at the moment the exception was captured. Note that heavy use of contexts will lead to truncated stack traces, as the exception will be captured by the context entered most recently, which may not be the point you catch the exception for the final time in your code. It's recommended to use contexts sparingly, for this reason. `capture_exception` takes the same set of optional arguments as `capture`. + +### Parameters + +- **`exception`** (`BaseException`) - The exception to capture. If not provided, the current exception is captured via `sys.exc_info()` **kwargs: Optional capture arguments including distinct_id, properties, timestamp, uuid, groups, flags, send_feature_flags, and disable_geoip. +- **`kwargs?`** (`Unpack[OptionalCaptureArgs]`) + +### Returns + +- `Optional[str]` + +### Examples + +```python +# Capture exception +from posthog import capture_exception +try: + risky_operation() +except Exception as e: + capture_exception(e) +``` + +--- + +### Feature flags methods + +#### evaluate_flags() + +**Release Tag:** public + +Evaluate all feature flags for a user in a single call and return a :class:`FeatureFlagEvaluations` snapshot. Branch on ``.is_enabled()`` / ``.get_flag()`` and pass the same snapshot to ``capture()`` via the ``flags`` option so events carry the exact flag values the code branched on. Prefer this over repeated ``get_feature_flag()`` calls and over ``capture(send_feature_flags=True)`` — it consolidates flag evaluation into a single ``/flags`` request per incoming request. + +### Parameters + +- **`distinct_id`** (`Number`) - The user's distinct ID. If ``None``, falls back to the context distinct_id. If still unresolvable, returns an empty snapshot. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - Mapping of group type to group key. +- **`person_properties?`** (`dict[str, Any]`) - Person properties to use for evaluation. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - Group properties keyed by group type. +- **`only_evaluate_locally`** (`bool`) - If ``True``, never fall back to remote evaluation. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup. +- **`flag_keys?`** (`list[str]`) - Optional list of flag keys. When provided, only these flags are evaluated — the underlying ``/flags`` request asks the server for just this subset, which makes the response smaller and the request cheaper. Use this when you only need a handful of flags out of many. +- **`device_id?`** (`str`) - Optional device ID override. If not provided, falls back to the context device_id (which may be set via tracing headers). Used by experience-continuity flags to match users across distinct_id changes. + +### Returns + +- `FeatureFlagEvaluations` + +### Examples + +```python +from posthog import evaluate_flags, capture +flags = evaluate_flags("user_123", person_properties={"plan": "enterprise"}) +if flags.is_enabled("new-dashboard"): + render_new_dashboard() +capture("page_viewed", distinct_id="user_123", flags=flags) +``` + +--- + +#### feature_enabled() + +**Release Tag:** public + +Use feature flags to enable or disable features for users. + +**Notes:** + +You can call `posthog.load_feature_flags()` before to make sure you're not doing unexpected requests. + +### Parameters + +- **`key?`** (`str`) - The feature flag key +- **`distinct_id?`** (`Number`) - The user's distinct ID +- **`groups?`** (`Mapping[str, Union[str, int]]`) - Groups mapping +- **`person_properties?`** (`dict[str, Any]`) - Person properties +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - Group properties +- **`only_evaluate_locally`** (`bool`) - Whether to evaluate only locally +- **`send_feature_flag_events`** (`bool`) - Whether to send feature flag events +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup +- **`device_id?`** (`str`) - Optional device ID override for experience-continuity flags + +### Returns + +- `Optional[bool]` + +### Examples + +```python +# Boolean feature flag +from posthog import feature_enabled, get_feature_flag_payload +is_my_flag_enabled = feature_enabled('flag-key', 'distinct_id_of_your_user') +if is_my_flag_enabled: + matched_flag_payload = get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') +``` + +--- + +#### feature_flag_definitions() + +**Release Tag:** public + +Returns loaded feature flags. + +**Notes:** + +Returns loaded feature flags, if any. Helpful for debugging what flag information you have loaded. + +### Returns + +- `None` + +### Examples + +```python +from posthog import feature_flag_definitions +definitions = feature_flag_definitions() +``` + +--- + +#### get_all_flags() + +**Release Tag:** public + +Get all flags for a given user. + +**Notes:** + +Flags are key-value pairs where the key is the flag key and the value is the flag variant, or True, or False. + +### Parameters + +- **`distinct_id?`** (`Number`) - The user's distinct ID +- **`groups?`** (`Mapping[str, Union[str, int]]`) - Groups mapping +- **`person_properties?`** (`dict[str, Any]`) - Person properties +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - Group properties +- **`only_evaluate_locally`** (`bool`) - Whether to evaluate only locally +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup +- **`device_id?`** (`str`) - Optional device ID override for experience-continuity flags +- **`flag_keys_to_evaluate?`** (`list[str]`) - Optional list of flag keys to evaluate (evaluates all if None) + +### Returns + +- `Optional[dict[str, Union[bool, str]]]` + +### Examples + +```python +# All flags for user +from posthog import get_all_flags +get_all_flags('distinct_id_of_your_user') +``` + +--- + +#### get_all_flags_and_payloads() + +**Release Tag:** public + +Get all feature flag values and payloads for a user. + +### Parameters + +- **`distinct_id?`** (`Number`) - The user's distinct ID. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - Mapping of group type to group key. +- **`person_properties?`** (`dict[str, Any]`) - Person properties to use for evaluation. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - Group properties keyed by group type. +- **`only_evaluate_locally`** (`bool`) - Whether to evaluate only locally. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup. +- **`device_id?`** (`str`) - Optional device ID override for experience-continuity flags. +- **`flag_keys_to_evaluate?`** (`list[str]`) - Optional list of flag keys to evaluate. Evaluates all flags when omitted. + +### Returns + +- `FlagsAndPayloads` + +--- + +#### get_feature_flag() + +**Release Tag:** public + +Get feature flag variant for users. Used with experiments. + +**Notes:** + +`groups` are a mapping from group type to group key. So, if you have a group type of "organization" and a group key of "5", you would pass groups={"organization": "5"}. `group_properties` take the format: { group_type_name: { group_properties } }. So, for example, if you have the group type "organization" and the group key "5", with the properties name, and employee count, you'll send these as: group_properties={"organization": {"name": "PostHog", "employees": 11}}. + +### Parameters + +- **`key?`** (`str`) - The feature flag key +- **`distinct_id?`** (`Number`) - The user's distinct ID +- **`groups?`** (`Mapping[str, Union[str, int]]`) - Groups mapping from group type to group key +- **`person_properties?`** (`dict[str, Any]`) - Person properties +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - Group properties in format { group_type_name: { group_properties } } +- **`only_evaluate_locally`** (`bool`) - Whether to evaluate only locally +- **`send_feature_flag_events`** (`bool`) - Whether to send feature flag events +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup +- **`device_id?`** (`str`) - Optional device ID override for experience-continuity flags + +### Returns + +- `Union[bool, str, any]` + +### Examples + +```python +# Multivariate feature flag +from posthog import get_feature_flag, get_feature_flag_payload +enabled_variant = get_feature_flag('flag-key', 'distinct_id_of_your_user') +if enabled_variant == 'variant-key': + matched_flag_payload = get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') +``` + +--- + +#### get_feature_flag_payload() + +**Release Tag:** public + +Get the payload associated with a feature flag value. Deprecated for new code. Prefer ``evaluate_flags()`` and ``flags.get_flag_payload(key)`` so flag evaluation happens once per request. + +### Parameters + +- **`key?`** (`str`) - The feature flag key. +- **`distinct_id?`** (`Number`) - The user's distinct ID. +- **`match_value`** (`bool`) - Optional flag value to use when selecting a payload. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - Mapping of group type to group key. +- **`person_properties?`** (`dict[str, Any]`) - Person properties to use for evaluation. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - Group properties keyed by group type. +- **`only_evaluate_locally`** (`bool`) - Whether to evaluate only locally. +- **`send_feature_flag_events`** (`bool`) - Whether to send a $feature_flag_called event. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup. +- **`device_id?`** (`str`) - Optional device ID override for experience-continuity flags. + +### Returns + +- `Optional[object]` + +--- + +#### load_feature_flags() + +**Release Tag:** public + +Load feature flag definitions from PostHog. + +### Returns + +- `None` + +### Examples + +```python +from posthog import load_feature_flags +load_feature_flags() +``` + +--- + +### Client management methods + +#### flush() + +**Release Tag:** public + +Tell the client to flush all queued events. + +### Parameters + +- **`timeout_seconds?`** (`float`) - Maximum seconds to wait for the queue to flush. Defaults to 10 seconds. Pass ``None`` to wait indefinitely. + +### Returns + +- `any` + +### Examples + +```python +from posthog import flush +flush() +``` + +--- + +#### join() + +**Release Tag:** public + +Block program until the client clears the queue. Used during program shutdown. You should use `shutdown()` directly in most cases. + +### Returns + +- `any` + +### Examples + +```python +from posthog import join +join() +``` + +--- + +#### shutdown() + +**Release Tag:** public + +Flush all messages and cleanly shutdown the client. + +### Returns + +- `any` + +### Examples + +```python +from posthog import shutdown +shutdown() +``` + +--- + +### Other methods + +#### get_feature_flag_result() + +**Release Tag:** public + +Get a FeatureFlagResult object which contains the flag result and payload. This method evaluates a feature flag and returns a FeatureFlagResult object containing: - enabled: Whether the flag is enabled - variant: The variant value if the flag has variants - payload: The payload associated with the flag (automatically deserialized from JSON) - key: The flag key - reason: Why the flag was enabled/disabled + +### Parameters + +- **`key?`** (`str`) - The feature flag key. +- **`distinct_id?`** (`Number`) - The user's distinct ID. +- **`groups?`** (`Mapping[str, Union[str, int]]`) - Mapping of group type to group key. +- **`person_properties?`** (`dict[str, Any]`) - Person properties to use for evaluation. +- **`group_properties?`** (`dict[str, dict[str, Any]]`) - Group properties keyed by group type. +- **`only_evaluate_locally`** (`bool`) - Whether to evaluate only locally. +- **`send_feature_flag_events`** (`bool`) - Whether to send a $feature_flag_called event. +- **`disable_geoip?`** (`bool`) - Whether to disable GeoIP lookup. +- **`device_id?`** (`str`) - Optional device ID override for experience-continuity flags. + +### Returns + +- `Optional[FeatureFlagResult]` + +--- + +#### get_remote_config_payload() + +**Release Tag:** public + +Get the payload for a remote config feature flag. + +### Parameters + +- **`key?`** (`str`) - The key of the feature flag + +### Returns + +- `None` + +--- + +#### set_code_variables_mask_url_credentials_context() + +**Release Tag:** public + +Whether to scrub credentials embedded in URLs/DSNs (e.g. user:pass@host) from captured code variables for the current context. + +### Parameters + +- **`enabled?`** (`bool`) + +### Returns + +- `None` + +--- + +### Contexts methods + +#### get_tags() + +**Release Tag:** public + +Get all tags from the current context. Returns: Dict of all tags in the current context + +### Returns + +- `dict[str, Any]` + +--- + +#### new_context() + +**Release Tag:** public + +Create a new context scope that will be active for the duration of the with block. + +### Parameters + +- **`fresh`** (`bool`) - Whether to start with a fresh context (default: False) +- **`capture_exceptions?`** (`bool`) - Whether to capture exceptions raised within the context. If omitted, defaults to the relevant client's exception autocapture setting. +- **`client?`** (`Client`) - Optional Posthog client instance to use for this context (default: None) + +### Returns + +- `None` + +### Examples + +```python +from posthog import new_context, tag, capture +with new_context(): + tag("request_id", "123") + capture("event_name", properties={"property": "value"}) +``` + +--- + +#### scoped() + +**Release Tag:** public + +Decorator that creates a new context for the function. + +### Parameters + +- **`fresh`** (`bool`) - Whether to start with a fresh context (default: False) +- **`capture_exceptions?`** (`bool`) - Whether to capture and track exceptions with posthog error tracking. If omitted, defaults to the global exception autocapture setting. + +### Returns + +- `None` + +### Examples + +```python +from posthog import scoped, tag, capture +@scoped() +def process_payment(payment_id): + tag("payment_id", payment_id) + capture("payment_started") +``` + +--- + +#### set_capture_exception_code_variables_context() + +**Release Tag:** public + +Override code-variable capture for exceptions in the current context. + +### Parameters + +- **`enabled?`** (`bool`) - Whether exceptions captured in this context should include local variable values from stack frames. + +### Returns + +- `None` + +--- + +#### set_code_variables_detect_secrets_context() + +**Release Tag:** public + +Whether to apply entropy-based secret detection as a last-resort redaction of high-entropy values (API keys, tokens, strong passwords) in captured code variables for the current context. + +### Parameters + +- **`enabled?`** (`bool`) + +### Returns + +- `None` + +--- + +#### set_code_variables_ignore_patterns_context() + +**Release Tag:** public + +Override code-variable ignore patterns for exceptions in the current context. + +### Parameters + +- **`ignore_patterns?`** (`list[str]`) - Variable-name patterns that should be omitted entirely when code variables are captured. + +### Returns + +- `None` + +--- + +#### set_code_variables_mask_patterns_context() + +**Release Tag:** public + +Override code-variable mask patterns for exceptions in the current context. + +### Parameters + +- **`mask_patterns?`** (`list[str]`) - Variable-name patterns whose values should be replaced with ``***`` when code variables are captured. + +### Returns + +- `None` + +--- + +#### set_context_device_id() + +**Release Tag:** public + +Set the device ID for the current context, associating all feature flag requests in this or child contexts with the given device ID. + +### Parameters + +- **`device_id?`** (`str`) - The device ID to associate with the current context and its children + +### Returns + +- `None` + +### Examples + +```python +from posthog import set_context_device_id +set_context_device_id("device_123") +``` + +--- + +#### set_context_session() + +**Release Tag:** public + +Set the session ID for the current context. + +### Parameters + +- **`session_id?`** (`str`) - The session ID to associate with the current context and its children + +### Returns + +- `None` + +### Examples + +```python +from posthog import set_context_session +set_context_session("session_123") +``` + +--- + +#### tag() + +**Release Tag:** public + +Add a tag to the current context. + +### Parameters + +- **`name?`** (`str`) - The tag key +- **`value?`** (`Any`) - The tag value + +### Returns + +- `None` + +### Examples + +```python +from posthog import tag +tag("user_id", "123") +``` + +--- + +### Initialization methods + +#### setup() + +**Release Tag:** public + +Create or return the global PostHog client configured by module settings. Most applications should either instantiate ``Posthog`` directly or set ``posthog.api_key``/other module settings before calling top-level helpers. ``setup()`` is called automatically by global APIs such as ``capture()``. Returns: The global ``Client`` instance. If ``api_key`` is missing or blank, the client is disabled and module-level calls become no-ops. + +### Returns + +- `Client` + +--- \ No newline at end of file diff --git a/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/python.md b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/python.md new file mode 100644 index 000000000..63c0a98f4 --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/.claude/skills/integration-python/references/python.md @@ -0,0 +1,884 @@ +# Python - Docs + +The Python SDK makes it easy to capture events, evaluate feature flags, track errors, and more in your Python apps. + +**Python 3.9 and lower** + +Python 3.9 is no longer supported for PostHog Python SDK versions `7.x.x` and higher. + +## Installation + +Terminal + +PostHog AI + +```bash +pip install posthog +``` + +**Upgrading to v6** + +Version `6.x` of the PostHog Python SDK introduces a new [contexts](/docs/libraries/python.md#contexts) API and breaking changes. If you're upgrading from `5.x` to `6.x`, read the [migration guide](/tutorials/python-v6-migration.md) first to learn more. + +In your app, import the `posthog` library and set your project token and host **before** making any calls. + +Python + +PostHog AI + +```python +from posthog import Posthog +posthog = Posthog('', host='https://us.i.posthog.com') +``` + +> **Note:** As a rule of thumb, we do not recommend having API keys or tokens in plaintext. Setting it as an environment variable is best. + +You can find your project token and instance address in the [project settings](https://app.posthog.com/project/settings) page in PostHog. + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` that matches the ID your frontend uses when calling `posthog.identify()`. Without this, backend events are orphaned — they can't be linked to frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), or [error tracking](/docs/error-tracking.md). +> +> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. + +## Capturing events + +You can send custom events using `capture`: + +Python + +PostHog AI + +```python +# Events captured with no context or explicit distinct_id are marked as personless and have an auto-generated distinct_id: +posthog.capture('some-anon-event') +from posthog import identify_context, new_context +# Use contexts to manage user identification across multiple capture calls +with new_context(): + identify_context('distinct_id_of_the_user') + posthog.capture('user_signed_up') + posthog.capture('user_logged_in') + # You can also capture events with a specific distinct_id + posthog.capture('some-custom-action', distinct_id='distinct_id_of_the_user') +``` + +> **Tip:** We recommend using a `[object] [verb]` format for your event names, where `[object]` is the entity that the behavior relates to, and `[verb]` is the behavior itself. For example, `project created`, `user signed up`, or `invite sent`. + +> **Tip:** You can define event schemas with typed properties and generate type-safe code using [schema management](/docs/product-analytics/schema-management.md). + +### Setting event properties + +Optionally, you can include additional information with the event by including a [properties](/docs/data/events.md#event-properties) object: + +Python + +PostHog AI + +```python +posthog.capture( + "user_signed_up", + distinct_id="distinct_id_of_the_user", + properties={ + "login_type": "email", + "is_free_trial": "true" + } +) +``` + +### Sending page views + +If you're aiming for a backend-only implementation of PostHog and won't be capturing events from your frontend, you can send `pageviews` from your backend like so: + +Python + +PostHog AI + +```python +posthog.capture('$pageview', distinct_id="distinct_id_of_the_user", properties={'$current_url': 'https://example.com'}) +``` + +## Person profiles and properties + +The Python SDK captures identified events if the current context is identified or if you pass a distinct ID explicitly. These create [person profiles](/docs/data/persons.md). To set [person properties](/docs/data/user-properties.md) in these profiles, include them when capturing an event: + +Python + +PostHog AI + +```python +# Passing a distinct id explicitly +posthog.capture( + 'event_name', + distinct_id='user-distinct-id', + properties={ + '$set': {'name': 'Max Hedgehog'}, + '$set_once': {'initial_url': '/blog'} + } +) +# Using contexts +from posthog import new_context, identify_context +with new_context(): + identify_context('user-distinct-id') + posthog.capture('event_name') +``` + +For more details on the difference between `$set` and `$set_once`, see our [person properties docs](/docs/data/user-properties.md#what-is-the-difference-between-set-and-set_once). + +To capture [anonymous events](/docs/data/anonymous-vs-identified-events.md) without person profiles, set the event's `$process_person_profile` property to `False`. Events captured with no context or explicit distinct\_id are marked as personless, and will have an auto-generated distinct\_id: + +Python + +PostHog AI + +```python +posthog.capture( + event='event_name', + properties={ + '$process_person_profile': False + } +) +``` + +## Alias + +Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend. + +In this case, you can use `alias` to assign another distinct ID to the same user. + +Python + +PostHog AI + +```python +posthog.alias(previous_id='distinct_id', distinct_id='alias_id') +``` + +We strongly recommend reading our docs on [alias](/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) to best understand how to correctly use this method. + +## Contexts + +The Python SDK uses nested contexts for managing state that's shared across events. Contexts are the recommended way to manage things like "which user is taking this action" (through `identify_context`), rather than manually passing user state through your apps stack. + +When events (including exceptions) are captured in a context, the event uses the user [distinct ID](/docs/getting-started/identify-users.md), [session ID](/docs/data/sessions.md), and tags that are (optionally) set in the context. This is useful for adding properties to multiple events during a single user's interaction with your product. + +You can enter a context using the `with` statement: + +Python + +PostHog AI + +```python +from posthog import new_context, tag, set_context_session, identify_context +with new_context(): + tag("transaction_id", "abc123") + tag("some_arbitrary_value", {"tags": "can be dicts"}) + # Sessions are UUIDv7 values and used to track a sequence of events that occur within a single user session + # See https://posthog.com/docs/data/sessions + set_context_session(session_id) + # Setting the context-level distinct ID. See below for more details. + identify_context(user_id) + # This event is captured with the distinct ID, session ID, and tags set above + posthog.capture("order_processed") +``` + +Contexts are persisted across function calls. If you enter one and then call a function and capture an event in the called function, it uses the context tags and session ID set in the parent context: + +Python + +PostHog AI + +```python +from posthog import new_context, tag +def some_function(): + # When called from `outer_function`, this event is captured with the property some-key="value-4" + posthog.capture("order_processed") +def outer_function(): + with new_context(): + tag("some-key", "value-4") + some_function() +``` + +Contexts are nested, so tags added to a parent context are inherited by child contexts. If you set the same tag in both a parent and child context, the child context's value overrides the parent's at event capture (but the parent context won't be affected). This nesting also applies to session IDs and distinct IDs. + +Python + +PostHog AI + +```python +from posthog import new_context, tag +with new_context(): + tag("some-key", "value-1") + tag("some-other-key", "another-value") + with new_context(): + tag("some-key", "value-2") + # This event is captured with some-key="value-2" and some-other-key="another-value" + posthog.capture("order_processed") + # This event is captured with some-key="value-1" and some-other-key="another-value" + posthog.capture("order_processed") +``` + +You can disable this nesting behavior by passing `fresh=True` to `new_context`: + +Python + +PostHog AI + +```python +from posthog import new_context, tag +with new_context(fresh=True): + tag("some-key", "value-2") + # This event only has the property some-key="value-2" from the fresh context + posthog.capture("order_processed") +``` + +> **Note:** Distinct IDs, session IDs, and properties passed directly to calls to `capture` and related functions override context state in the final event captured. + +### Contexts and user identification + +Contexts can be associated with a distinct ID by calling `posthog.identify_context`: + +Python + +PostHog AI + +```python +from posthog import identify_context +identify_context("distinct-id") +``` + +Within a context associated with a distinct ID, all events captured are associated with that user. You can override the distinct ID for a specific event by passing a `distinct_id` argument to `capture`: + +Python + +PostHog AI + +```python +from posthog import new_context, identify_context +with new_context(): + identify_context("distinct-id") + posthog.capture("order_processed") # will be associated with distinct-id + posthog.capture("order_processed", distinct_id="another-distinct-id") # will be associated with another-distinct-id +``` + +It's recommended to pass the currently active distinct ID from the frontend to the backend, using the `X-POSTHOG-DISTINCT-ID` header. If you're using our Django middleware, this is extracted and associated with the request handler context automatically. + +You can read more about identifying users in the [user identification documentation](/docs/product-analytics/identify.md). + +### Contexts and sessions + +Contexts can be associated with a session ID by calling `posthog.set_context_session`. When linking backend events to frontend sessions, use the session ID from the frontend SDK (PostHog session IDs are UUIDv7 strings). + +Python + +PostHog AI + +```python +from posthog import new_context, set_context_session +with new_context(): + set_context_session(request.get_header("X-POSTHOG-SESSION-ID")) +``` + +**Using PostHog on your frontend too?** + +If you're using the PostHog JavaScript Web SDK on your frontend, it generates a session ID for you. Configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your backend hostname to add the session and distinct ID headers to browser requests automatically. + +You need to extract the header in your request handler (if you're using our Django middleware integration, this happens automatically). + +If you associate a context with a session, you'll be able to do things like: + +- See backend events on the session timeline when viewing session replays +- View session replays for users that triggered a backend exception in error tracking + +You can read more about sessions in the [session tracking](/docs/data/sessions.md) documentation. + +### Exception capture + +By default exceptions raised within a context are captured and available in the [error tracking](/docs/error-tracking.md) dashboard. You can override this behavior by passing `capture_exceptions=False` to `new_context`: + +Python + +PostHog AI + +```python +from posthog import new_context, tag +with new_context(capture_exceptions=False): + tag("transaction_id", "abc123") + tag("some_arbitrary_value", {"tags": "can be dicts"}) + # This event will be captured with the tags set above + posthog.capture("order_processed") + # This exception will not be captured + raise Exception("Order processing failed") +``` + +### Decorating functions + +The SDK exposes a function decorator. It takes the same `fresh` and `capture_exceptions` arguments as `new_context` and provides a handy way to mark a whole function as being in a new context. For example: + +Python + +PostHog AI + +```python +from posthog import scoped, identify_context +@scoped(fresh=True) +def process_order(user, order_id): + identify_context(user.distinct_id) + posthog.capture("order_processed") # Associated with the user + raise Exception("Order processing failed") # This exception is also captured and associated with the user +``` + +## Group analytics + +Group analytics allows you to associate an event with a group (e.g. teams, organizations, etc.). Read the [Group Analytics](/docs/user-guides/group-analytics.md) guide for more information. + +> **Note:** This is a paid feature and is not available on the open-source or free cloud plan. Learn more on our [pricing page](/pricing.md). + +To capture an event and associate it with a group: + +Python + +PostHog AI + +```python +posthog.capture('some_event', groups={'company': 'company_id_in_your_db'}) +``` + +To update properties on a group: + +Python + +PostHog AI + +```python +posthog.group_identify('company', 'company_id_in_your_db', { + 'name': 'Awesome Inc.', + 'employees': 11 +}) +``` + +The `name` is a special property which is used in the PostHog UI for the name of the group. If you don't specify a `name` property, the group ID will be used instead. + +## Feature flags + +PostHog's [feature flags](/docs/feature-flags.md) enable you to safely deploy and roll back new features as well as target specific users and groups with them. + +There are two steps to implement feature flags in Python: + +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. + +#### Boolean feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +#### Multivariate feature flags + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +enabled_variant = flags.get_flag("flag-key") +if enabled_variant == "variant-key": # replace "variant-key" with the key of your variant + # Do something differently for this user + # Optional: fetch the payload + matched_flag_payload = flags.get_flag_payload("flag-key") +``` + +`flags.get_flag()` returns the variant string for multivariate flags, `True` for enabled boolean flags, `False` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `posthog.capture(send_feature_flags=True)` still work during the migration period, but they're deprecated. Prefer `posthog.evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("distinct_id_of_your_user") +if flags.is_enabled("flag-key"): + # Do something differently for this user + pass +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags, +) +``` + +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Python + +PostHog AI + +```python +# Attach only flags accessed with is_enabled() or get_flag() before this call +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only_accessed(), +) +# Attach only specific flags +posthog.capture( + "event_name", + distinct_id="distinct_id_of_your_user", + flags=flags.only(["checkout-flow", "new-dashboard"]), +) +``` + +`only_accessed()` is order-dependent. If you call it before accessing any flags with `is_enabled()` or `get_flag()`, no feature flag properties are attached. + +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: + +Python + +PostHog AI + +```python +posthog.capture( + "event_name", + distinct_id="distinct_id_of_the_user", + properties={ + # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant + "$feature/feature-flag-key": "variant-key", + }, +) +``` + +### Evaluating only specific flags + +By default, `posthog.evaluate_flags()` evaluates every flag for the user. If you only need a few flags, pass `flag_keys` to request only those flags: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_your_user", + flag_keys=["checkout-flow", "new-dashboard"], +) +``` + +### Sending `$feature_flag_called` events + +Capturing `$feature_flag_called` events enables PostHog to know when a flag was accessed by a user and provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. With `posthog.evaluate_flags()`, the SDK sends this event when you call `flags.is_enabled()` or `flags.get_flag()` for a flag. + +The SDK deduplicates these events per `(distinct_id, flag, value)` in a local cache. If you reinitialize the PostHog client, the cache resets and `$feature_flag_called` events may be sent again. PostHog handles duplicates, so duplicate `$feature_flag_called` events don't affect your analytics. + +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. + +### Advanced: Overriding server properties + +Sometimes, you may want to evaluate feature flags using [person properties](/docs/product-analytics/person-properties.md), [groups](/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier. + +You can provide properties to evaluate the flag with by using the `person properties`, `groups`, and `group properties` arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server. + +For example: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags( + "distinct_id_of_the_user", + person_properties={"property_name": "value"}, + groups={ + "your_group_type": "your_group_id", + "another_group_type": "your_group_id", + }, + group_properties={ + "your_group_type": {"group_property_name": "value"}, + "another_group_type": {"group_property_name": "value"}, + }, +) +if flags.is_enabled("flag-key"): + # Do something differently for this user +``` + +### Overriding GeoIP properties + +By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties. + +You can override GeoIP properties by including them in the `person_properties` parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location. + +The following GeoIP properties can be overridden: + +- `$geoip_country_code` +- `$geoip_country_name` +- `$geoip_city_name` +- `$geoip_city_confidence` +- `$geoip_continent_code` +- `$geoip_continent_name` +- `$geoip_latitude` +- `$geoip_longitude` +- `$geoip_postal_code` +- `$geoip_subdivision_1_code` +- `$geoip_subdivision_1_name` +- `$geoip_subdivision_2_code` +- `$geoip_subdivision_2_name` +- `$geoip_subdivision_3_code` +- `$geoip_subdivision_3_name` +- `$geoip_time_zone` + +Simply include any of these properties in the `person_properties` parameter alongside your other person properties when calling feature flags. + +### Request timeout + +You can configure the `feature_flags_request_timeout_seconds` parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds. + +Python + +PostHog AI + +```python +posthog = Posthog( + "", + host="https://us.i.posthog.com", + feature_flags_request_timeout_seconds=3, # Time in seconds. Defaults to 3. +) +``` + +### Local evaluation + +Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests. + +It is best practice to use local evaluation flags when possible, since this enables you to resolve flags faster and with fewer API calls. + +For details on how to implement local evaluation, see our [local evaluation guide](/docs/feature-flags/local-evaluation.md). + +#### Distributed environments + +In multi-worker or edge environments, you can implement custom caching for flag definitions using Redis, Cloudflare KV, or other storage backends. This enables sharing definitions across workers and coordinating fetches. See our guide for [local evaluation in distributed environments](/docs/feature-flags/local-evaluation/distributed-environments?tab=Python.md) for details. + +## Experiments (A/B tests) + +Since [experiments](/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code: + +Python + +PostHog AI + +```python +flags = posthog.evaluate_flags("user_distinct_id") +variant = flags.get_flag("experiment-feature-flag-key") +if variant == "variant-name": + # Do something +``` + +It's also possible to [run experiments without using feature flags](/docs/experiments/running-experiments-without-feature-flags.md). + +## AI Observability + +Our Python SDK includes a built-in AI Observability feature. It enables you to capture LLM usage, performance, and more. Check out our [analytics docs](/docs/ai-observability.md) for more details on setting it up. + +## Error tracking + +You can [autocapture exceptions](/docs/error-tracking/installation.md) by setting the `enable_exception_autocapture` argument to `True` when initializing the PostHog client. + +Python + +PostHog AI + +```python +from posthog import Posthog +posthog = Posthog("", enable_exception_autocapture=True, ...) +``` + +You can also manually capture exceptions using the `capture_exception` method: + +Python + +PostHog AI + +```python +posthog.capture_exception(e, distinct_id='user_distinct_id', properties=additional_properties) +``` + +Contexts automatically capture exceptions thrown inside them, unless disable it by passing `capture_exceptions=False` to `new_context()`. + +### Code variables capture + +The Python SDK can automatically capture the state of local variables when an exception occurs. This gives you a debugger-like view of your application state at the time of the error: + +Python + +PostHog AI + +```python +posthog = Posthog( + "", + enable_exception_autocapture=True, + capture_exception_code_variables=True, +) +``` + +You can configure which variables are captured, masked, or ignored. See the [code variables documentation](/docs/error-tracking/code-variables/python.md) for detailed configuration options. + +## GeoIP properties + +Before posthog-python v3.0, we added GeoIP properties to all incoming events by default. We also used these properties for feature flag evaluation, based on the IP address of the request. This isn't ideal since they are created based on your server IP address, rather than the user's, leading to incorrect location resolution. + +As of posthog-python v3.0, the default now is to disregard the server IP, not add the GeoIP properties, and not use the values for feature flag evaluations. + +You can go back to previous behavior by doing setting the `disable_geoip` argument in your initialization to `False`: + +Python + +PostHog AI + +```python +posthog = Posthog('api_key', disable_geoip=False) +``` + +The list of properties that this overrides: + +1. `$geoip_city_name` +2. `$geoip_country_name` +3. `$geoip_country_code` +4. `$geoip_continent_name` +5. `$geoip_continent_code` +6. `$geoip_postal_code` +7. `$geoip_time_zone` + +You can also explicitly chose to enable or disable GeoIP for a single capture request like so: + +Python + +PostHog AI + +```python +posthog.capture('test_event', disable_geoip=True|False) +``` + +## Debug mode + +If you're not seeing the expected events being captured, the feature flags being evaluated, or the surveys being shown, you can enable debug mode to see what's happening. + +You can enable debug mode by setting the `debug` option to `True` in the `PostHog` object. This will enable verbose logs about the inner workings of the SDK. + +Python + +PostHog AI + +```python +posthog.debug = True +``` + +## Disabling requests during tests + +You can disable requests during tests by setting the `disabled` option to `True` in the `PostHog` object. This means no events will be captured or no requests will be sent to PostHog. + +Python + +PostHog AI + +```python +if settings.TEST: + posthog.disabled = True +``` + +## Connection configuration + +The SDK uses HTTP connection pooling internally for better performance. These settings typically need not be changed, but in some environments, such as when running behind NAT gateways, pooled connections may be terminated non-gracefully, causing request failures. + +You can configure connection behavior in several ways. The following settings should be called during initialization, before any API requests are made. + +### Enable TCP keepalive + +TCP keepalive probes help prevent idle connections from being dropped by network infrastructure. This is the recommended approach for most cases where idle connections are terminated. + +Python + +PostHog AI + +```python +import posthog +posthog.enable_keep_alive() +``` + +This enables TCP keepalive with sensible defaults (60 second idle time, 60 second probe interval, 3 probes before timeout). + +### Disable connection pooling + +If you need each request to use a fresh connection, you can disable connection reuse entirely. This will incur additional overhead per request but may be desirable in some circumstances. + +Python + +PostHog AI + +```python +import posthog +posthog.disable_connection_reuse() +``` + +### Custom HTTP socket options + +For advanced use cases, you can configure arbitrary socket options on the underlying HTTP connection. + +Python + +PostHog AI + +```python +import socket +import posthog +posthog.set_socket_options([ + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + # Add additional socket options as needed +]) +``` + +Pass `None` to `set_socket_options()` to reset to default behavior. + +## Filtering or modifying events before sending + +Use `before_send` to modify or drop events before they are queued for delivery. Return the modified event dictionary to send it, or `None` to drop it. + +Python + +PostHog AI + +```python +from typing import Any +import posthog +def scrub_pii(event: dict[str, Any]) -> dict[str, Any] | None: + properties = event.get("properties", {}) + if "email" in properties: + email = properties["email"] + properties["email"] = f"***@{email.split('@', 1)[1]}" if "@" in email else "***" + if event.get("event") == "test_event": + return None + return event +client = posthog.Client( + "", + before_send=scrub_pii, +) +``` + +If your callback raises an exception, the SDK logs the error and continues with the original unmodified event. + +## Historical migrations + +You can use the Python or Node SDK to run [historical migrations](/docs/migrate.md) of data into PostHog. To do so, set the `historical_migration` option to `true` when initializing the client. + +PostHog AI + +### Python + +```python +from posthog import Posthog +from datetime import datetime +posthog = Posthog( + '', + host='https://us.i.posthog.com', + debug=True, + historical_migration=True +) +events = [ + { + "event": "batched_event_name", + "properties": { + "distinct_id": "user_id", + "timestamp": datetime.fromisoformat("2024-04-02T12:00:00") + } + }, + { + "event": "batched_event_name", + "properties": { + "distinct_id": "used_id", + "timestamp": datetime.fromisoformat("2024-04-02T12:00:00") + } + } +] +for event in events: + posthog.capture( + distinct_id=event["properties"]["distinct_id"], + event=event["event"], + properties=event["properties"], + timestamp=event["properties"]["timestamp"], + ) +``` + +### Node.js + +```javascript +import { PostHog } from 'posthog-node' +const client = new PostHog( + '', + { + host: 'https://us.i.posthog.com', + historicalMigration: true + } +) +client.debug() +client.capture({ + event: "batched_event_name", + distinctId: "user_id", + properties: {}, + timestamp: "2024-04-03T12:00:00Z" +}) +client.capture({ + event: "batched_event_name", + distinctId: "user_id", + properties: {}, + timestamp: "2024-04-03T13:00:00Z" +}) +await client.shutdown() +``` + +## Serverless environments (Render/Lambda/...) + +By default, the library buffers events before sending them to the capture endpoint, for better performance. This can lead to lost events in serverless environments, if the Python process is terminated by the platform before the buffer is fully flushed. To avoid this, you can either: + +- Ensure that `posthog.shutdown()` is called after processing every request by adding a middleware to your server. This allows `posthog.capture()` to remain asynchronous for better performance. `posthog.shutdown()` is blocking. +- Enable the `sync_mode` option when initializing the client, so that all calls to `posthog.capture()` become synchronous. + +## Django + +See our [Django docs](/docs/libraries/django.md) for how to set up PostHog in Django. Our library includes a [contexts middleware](/docs/libraries/django.md#django-contexts-middleware) that can automatically capture distinct IDs, session IDs, and other properties you can set up with tags. + +## Alternative name + +As our open source project [PostHog](https://github.com/PostHog/posthog) shares the same module name, we created a special `posthoganalytics` package, mostly for internal use to avoid module collision. It is the exact same. + +## Thank you + +This library is largely based on the `analytics-python` package. + +### Community questions + +Ask a question + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/python/meeting-summarizer/posthog-setup-report.md b/apps/basic-integration/python/meeting-summarizer/posthog-setup-report.md new file mode 100644 index 000000000..e5b9b2150 --- /dev/null +++ b/apps/basic-integration/python/meeting-summarizer/posthog-setup-report.md @@ -0,0 +1,41 @@ + +# PostHog post-wizard report + +The wizard has completed a deep integration of this pure Python meeting summarizer application. PostHog was added to the backend web server and background user service using environment-based configuration, exception autocapture, person property syncing, and graceful shutdown handling. Client-side analytics initialization was added to the login and dashboard flows using a server-provided config endpoint so browser code does not hardcode credentials. Product analytics now cover authentication, dashboard engagement, meeting creation and deletion, meeting detail views, and user lifecycle operations. + +| Event name | Description | File | +| --- | --- | --- | +| user_logged_in | Captures successful sign-in attempts for authenticated users. | server.py | +| login_failed | Captures failed sign-in attempts when credentials do not match an active user. | server.py | +| meeting_created | Captures successful transcript uploads and AI summary generation for a meeting. | server.py | +| meeting_deleted | Captures when a user permanently deletes a meeting record. | server.py | +| meeting_viewed | Captures when a user opens a meeting detail view from the dashboard. | static/app.js | +| meeting_upload_started | Captures when a user submits a new transcript for analysis. | static/app.js | +| dashboard_loaded | Captures when an authenticated user loads the meetings dashboard. | static/app.js | +| logout_clicked | Captures when a user initiates logout from the dashboard. | static/app.js | +| user_registered | Captures successful user creation through the background user management flow. | user_service.py | +| user_deactivated | Captures when a user account is deactivated in the user management flow. | user_service.py | + +## Next steps + +We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: + +- Dashboard: https://us.posthog.com/project/483112/dashboard/1807674 +- Insight: Meeting engagement funnel — https://us.posthog.com/project/483112/insights/hL6t8aBX +- Insight: Login success funnel — https://us.posthog.com/project/483112/insights/DbMp2Ucx +- Insight: Meeting creation volume — https://us.posthog.com/project/483112/insights/Pls1ZiUZ +- Insight: Meeting lifecycle outcomes — https://us.posthog.com/project/483112/insights/W3LeA3KL +- Insight: Logins over time — https://us.posthog.com/project/483112/insights/RXBAdQuD + +## Verify before merging + +- [ ] Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code. +- [ ] Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures. +- [ ] Add the exact PostHog env var names you added to `.env.example` and any monorepo/bootstrap scripts so collaborators know what to set. +- [ ] Confirm the returning-visitor path also calls `identify` — a handler that only identifies on fresh login can leave returning sessions on anonymous distinct IDs. + +### Agent skill + +We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. + + diff --git a/apps/basic-integration/python/meeting-summarizer/requirements.txt b/apps/basic-integration/python/meeting-summarizer/requirements.txt index 0082a96d6..b72f286dd 100644 --- a/apps/basic-integration/python/meeting-summarizer/requirements.txt +++ b/apps/basic-integration/python/meeting-summarizer/requirements.txt @@ -1,4 +1,4 @@ # User Service - Pure Python dependencies # AI Meeting Summarizer - Pure Python dependencies # This service uses only Python standard library -# No external dependencies required +posthog>=7.22.0 diff --git a/apps/basic-integration/python/meeting-summarizer/server.py b/apps/basic-integration/python/meeting-summarizer/server.py index 3b43b6572..300323565 100755 --- a/apps/basic-integration/python/meeting-summarizer/server.py +++ b/apps/basic-integration/python/meeting-summarizer/server.py @@ -4,6 +4,7 @@ Automatically summarize meetings with AI-powered analysis. """ +import atexit import json import logging import signal @@ -20,11 +21,32 @@ import traceback import mimetypes +from posthog import Posthog + from database import UserDatabase from models import User, Meeting from ai_summarizer import AISummarizer +def initialize_posthog(): + """Initialize PostHog client from environment variables.""" + project_token = os.getenv('POSTHOG_PROJECT_TOKEN') + if not project_token: + logging.warning("PostHog not configured: POSTHOG_PROJECT_TOKEN is missing") + return None + + return Posthog( + project_token, + host=os.getenv('POSTHOG_HOST'), + enable_exception_autocapture=True, + ) + + +posthog_client = initialize_posthog() +if posthog_client: + atexit.register(posthog_client.shutdown) + + # Session management class SessionManager: """Simple session management""" @@ -70,6 +92,55 @@ class SaaSHandler(BaseHTTPRequestHandler): db = UserDatabase() sessions = SessionManager() + def _get_posthog_distinct_id(self, user=None): + """Resolve a stable distinct ID for analytics.""" + if user: + return user.user_id + + session_id = self._get_session_id() + if session_id: + session = self.sessions.get_session(session_id) + if session: + return session['user_id'] + return None + + def _capture_event(self, event, distinct_id=None, properties=None): + """Safely capture a PostHog event when configured.""" + if not posthog_client or not distinct_id: + return + + posthog_client.capture( + event=event, + distinct_id=distinct_id, + properties=properties or {} + ) + + def _capture_exception(self, exception, distinct_id=None, properties=None): + """Safely capture an exception when PostHog is configured.""" + if not posthog_client: + return + + posthog_client.capture_exception( + exception, + distinct_id=distinct_id, + properties=properties or {} + ) + + def _set_person_properties(self, user): + """Set person properties for an authenticated user.""" + if not posthog_client or not user: + return + + posthog_client.set( + distinct_id=user.user_id, + properties={ + 'username': user.username, + 'email': user.email, + 'full_name': user.full_name, + 'is_active': user.is_active, + } + ) + def _set_headers(self, status_code=200, content_type='text/html'): """Set response headers""" self.send_response(status_code) @@ -153,6 +224,14 @@ def do_GET(self): self._serve_static_file('login.html') return + # API: PostHog frontend config + if path == '/api/posthog/config': + self._send_json({ + 'token': os.getenv('POSTHOG_PROJECT_TOKEN'), + 'host': os.getenv('POSTHOG_HOST'), + }) + return + # API: Check auth status if path == '/api/auth/status': user = self._get_current_user() @@ -243,6 +322,8 @@ def do_GET(self): self._serve_static_file(path) except Exception as e: + distinct_id = self._get_posthog_distinct_id(self._get_current_user()) + self._capture_exception(e, distinct_id=distinct_id, properties={'request_method': 'GET', 'request_path': self.path}) logging.error(f"Error in GET request: {e}\n{traceback.format_exc()}") self._send_json({'error': 'Internal server error'}, 500) @@ -273,6 +354,16 @@ def do_POST(self): logging.info(f"Login successful for: {email}") # Create session session_id = self.sessions.create_session(user.user_id) + self._set_person_properties(user) + self._capture_event( + 'user_logged_in', + distinct_id=user.user_id, + properties={ + 'login_method': 'password', + 'is_demo_user': user.username == 'demo', + 'has_full_name': bool(user.full_name), + } + ) # Send response with session cookie self.send_response(200) @@ -291,6 +382,18 @@ def do_POST(self): self.wfile.write(response_data.encode('utf-8')) else: logging.warning(f"Login failed for: {email} (user {'found but inactive' if user else 'not found'})") + self._capture_event( + 'login_failed', + distinct_id=f"login_attempt_{hashlib.sha256((email or 'unknown').encode('utf-8')).hexdigest()[:16]}", + properties={ + 'login_method': 'password', + 'email_domain': email.split('@', 1)[1] if email and '@' in email else 'unknown', + 'email_provided': bool(email), + 'password_provided': bool(password), + 'user_found': bool(user), + 'user_active': bool(user and user.is_active), + } + ) self._send_json({'error': 'User not found or inactive'}, 401) return @@ -370,6 +473,19 @@ def do_POST(self): ) if self.db.create_meeting(meeting): + self._capture_event( + 'meeting_created', + distinct_id=current_user.user_id, + properties={ + 'meeting_id': meeting.meeting_id, + 'title_length': len(meeting.title), + 'transcript_length': len(transcript), + 'action_item_count': len(action_items), + 'key_point_count': len(key_points), + 'participant_count': len(participants), + 'duration_minutes': duration, + } + ) self._send_json(meeting.to_dict(), 201) else: self._send_json({'error': 'Failed to create meeting'}, 500) @@ -380,6 +496,8 @@ def do_POST(self): except json.JSONDecodeError: self._send_json({'error': 'Invalid JSON'}, 400) except Exception as e: + distinct_id = self._get_posthog_distinct_id(self._get_current_user()) + self._capture_exception(e, distinct_id=distinct_id, properties={'request_method': 'POST', 'request_path': self.path}) logging.error(f"Error in POST request: {e}\n{traceback.format_exc()}") self._send_json({'error': 'Internal server error'}, 500) @@ -409,6 +527,8 @@ def do_PUT(self): self._send_json({'error': 'Not found'}, 404) except Exception as e: + distinct_id = self._get_posthog_distinct_id(self._get_current_user()) + self._capture_exception(e, distinct_id=distinct_id, properties={'request_method': 'PUT', 'request_path': self.path}) logging.error(f"Error in PUT request: {e}\n{traceback.format_exc()}") self._send_json({'error': 'Internal server error'}, 500) @@ -449,6 +569,16 @@ def do_DELETE(self): return if self.db.delete_meeting(meeting_id): + self._capture_event( + 'meeting_deleted', + distinct_id=current_user.user_id, + properties={ + 'meeting_id': meeting.meeting_id, + 'duration_minutes': meeting.duration_minutes, + 'action_item_count': len(meeting.action_items), + 'key_point_count': len(meeting.key_points), + } + ) self._send_json({'success': True}) else: self._send_json({'error': 'Failed to delete meeting'}, 500) @@ -457,6 +587,8 @@ def do_DELETE(self): self._send_json({'error': 'Not found'}, 404) except Exception as e: + distinct_id = self._get_posthog_distinct_id(self._get_current_user()) + self._capture_exception(e, distinct_id=distinct_id, properties={'request_method': 'DELETE', 'request_path': self.path}) logging.error(f"Error in DELETE request: {e}\n{traceback.format_exc()}") self._send_json({'error': 'Internal server error'}, 500) @@ -586,6 +718,8 @@ def main(): logging.info("Server stopped by user") finally: server.server_close() + if posthog_client: + posthog_client.shutdown() logging.info("Server shut down") diff --git a/apps/basic-integration/python/meeting-summarizer/static/app.js b/apps/basic-integration/python/meeting-summarizer/static/app.js index 25a4c6cb8..a58e72aa6 100644 --- a/apps/basic-integration/python/meeting-summarizer/static/app.js +++ b/apps/basic-integration/python/meeting-summarizer/static/app.js @@ -7,14 +7,20 @@ const uploadMeetingForm = document.getElementById('uploadMeetingForm'); const logoutBtn = document.getElementById('logoutBtn'); const toast = document.getElementById('toast'); +let posthog = null; + let currentUser = null; let allMeetings = []; let currentMeetingId = null; // Initialize document.addEventListener('DOMContentLoaded', async () => { + await initializePostHog(); await checkAuth(); await loadMeetings(); + captureEvent('dashboard_loaded', { + meeting_count: allMeetings.length, + }); await loadStats(); // Event listeners @@ -49,6 +55,13 @@ async function checkAuth() { } currentUser = data.user; + if (posthog) { + posthog.identify(currentUser.id, { + email: currentUser.email, + username: currentUser.username, + full_name: currentUser.full_name, + }); + } document.getElementById('currentUser').textContent = currentUser.email; } catch (error) { window.location.href = '/'; @@ -57,6 +70,9 @@ async function checkAuth() { async function handleLogout() { try { + captureEvent('logout_clicked', { + meeting_count: allMeetings.length, + }); await fetch('/api/auth/logout', { method: 'POST' }); window.location.href = '/'; } catch (error) { @@ -172,6 +188,11 @@ async function handleUploadMeeting(e) { transcript: formData.get('transcript'), }; + captureEvent('meeting_upload_started', { + title_length: meetingData.title.length, + transcript_length: meetingData.transcript.length, + }); + // Show loading state const submitBtn = uploadMeetingForm.querySelector('button[type="submit"]'); const originalText = submitBtn.textContent; @@ -220,6 +241,13 @@ async function openMeetingDetail(meetingId) { } currentMeetingId = meetingId; + captureEvent('meeting_viewed', { + meeting_id: meeting.meeting_id, + action_item_count: meeting.action_items.length, + key_point_count: meeting.key_points.length, + participant_count: meeting.participants.length, + duration_minutes: meeting.duration_minutes, + }); // Populate modal document.getElementById('detailTitle').textContent = meeting.title; @@ -312,6 +340,33 @@ function showToast(message, type = 'success') { }, 3000); } +function captureEvent(eventName, properties = {}) { + if (!posthog) { + return; + } + + posthog.capture(eventName, properties); +} + +async function initializePostHog() { + try { + const response = await fetch('/api/posthog/config'); + const config = await response.json(); + + if (!config.token || !config.host || !window.posthog) { + return; + } + + window.posthog.init(config.token, { + api_host: config.host, + person_profiles: 'identified_only', + }); + posthog = window.posthog; + } catch (error) { + posthog = null; + } +} + function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; diff --git a/apps/basic-integration/python/meeting-summarizer/static/login.js b/apps/basic-integration/python/meeting-summarizer/static/login.js index bc3a03024..f294afd94 100644 --- a/apps/basic-integration/python/meeting-summarizer/static/login.js +++ b/apps/basic-integration/python/meeting-summarizer/static/login.js @@ -1,6 +1,11 @@ // Login page JavaScript const loginForm = document.getElementById('loginForm'); const errorMessage = document.getElementById('errorMessage'); +let posthog = null; + +document.addEventListener('DOMContentLoaded', async () => { + await initializePostHog(); +}); loginForm.addEventListener('submit', async (e) => { e.preventDefault(); @@ -22,6 +27,12 @@ loginForm.addEventListener('submit', async (e) => { const data = await response.json(); if (response.ok) { + if (posthog && data.user) { + posthog.identify(data.user.id, { + email, + username: data.user.username, + }); + } window.location.href = '/'; } else { errorMessage.textContent = data.error || 'Login failed'; @@ -39,3 +50,22 @@ document.getElementById('password').addEventListener('keypress', (e) => { loginForm.dispatchEvent(new Event('submit')); } }); + +async function initializePostHog() { + try { + const response = await fetch('/api/posthog/config'); + const config = await response.json(); + + if (!config.token || !config.host || !window.posthog) { + return; + } + + window.posthog.init(config.token, { + api_host: config.host, + person_profiles: 'identified_only', + }); + posthog = window.posthog; + } catch (error) { + posthog = null; + } +} diff --git a/apps/basic-integration/python/meeting-summarizer/user_service.py b/apps/basic-integration/python/meeting-summarizer/user_service.py index 7ebd34a56..2df9fd729 100755 --- a/apps/basic-integration/python/meeting-summarizer/user_service.py +++ b/apps/basic-integration/python/meeting-summarizer/user_service.py @@ -1,14 +1,31 @@ #!/usr/bin/env python3 """User Management Service - A pure Python background service for managing users.""" +import atexit +import os import uuid from datetime import datetime from typing import Optional +from posthog import Posthog + from database import UserDatabase from models import User +def initialize_posthog(): + """Initialize PostHog client from environment variables.""" + project_token = os.getenv('POSTHOG_PROJECT_TOKEN') + if not project_token: + return None + + return Posthog( + project_token, + host=os.getenv('POSTHOG_HOST'), + enable_exception_autocapture=True, + ) + + class UserService: """Service for managing user lifecycle and operations.""" @@ -16,8 +33,48 @@ def __init__(self): """Initialize the user service.""" self.db = UserDatabase() self.service_id = str(uuid.uuid4()) + self.posthog_client = initialize_posthog() + if self.posthog_client: + atexit.register(self.posthog_client.shutdown) print(f"User service initialized (ID: {self.service_id})") + def _capture_event(self, event, distinct_id=None, properties=None): + """Capture analytics event when PostHog is configured.""" + if not self.posthog_client or not distinct_id: + return + + self.posthog_client.capture( + event=event, + distinct_id=distinct_id, + properties=properties or {} + ) + + def _capture_exception(self, exception, distinct_id=None, properties=None): + """Capture analytics exception when PostHog is configured.""" + if not self.posthog_client: + return + + self.posthog_client.capture_exception( + exception, + distinct_id=distinct_id, + properties=properties or {} + ) + + def _set_person_properties(self, user: User): + """Sync user traits to PostHog person properties.""" + if not self.posthog_client: + return + + self.posthog_client.set( + distinct_id=user.user_id, + properties={ + 'email': user.email, + 'username': user.username, + 'full_name': user.full_name, + 'is_active': user.is_active, + } + ) + def register_user(self, email: str, username: str, full_name: Optional[str] = None, metadata: Optional[dict] = None) -> Optional[User]: """Register a new user.""" user_id = str(uuid.uuid4()) @@ -35,6 +92,16 @@ def register_user(self, email: str, username: str, full_name: Optional[str] = No ) if self.db.create_user(user): + self._set_person_properties(user) + self._capture_event( + 'user_registered', + distinct_id=user.user_id, + properties={ + 'has_full_name': bool(full_name), + 'has_metadata': bool(metadata), + 'metadata_key_count': len(metadata or {}), + } + ) print(f"✓ User registered: {username} ({email})") return user else: @@ -62,6 +129,13 @@ def deactivate_user(self, user_id: str, reason: Optional[str] = None) -> bool: success = self.db.deactivate_user(user_id) if success: + self._capture_event( + 'user_deactivated', + distinct_id=user_id, + properties={ + 'reason_provided': bool(reason), + } + ) print(f"✓ User deactivated: {user_id}") else: print(f"✗ Failed to deactivate user: {user_id}") @@ -94,6 +168,8 @@ def export_users(self, active_only: bool = False) -> list: def shutdown(self): """Shutdown the service gracefully.""" + if self.posthog_client: + self.posthog_client.shutdown() print(f"Service {self.service_id} shutting down...") @@ -103,69 +179,77 @@ def main(): print("User Management Service") print("=" * 60) - # Initialize service - service = UserService() + service = None + try: + # Initialize service + service = UserService() - # Example usage: Register some test users - print("\n--- Registering Test Users ---") - - user1 = service.register_user( - email="alice@example.com", - username="alice", - full_name="Alice Smith", - metadata={'role': 'admin', 'department': 'engineering'} - ) + # Example usage: Register some test users + print("\n--- Registering Test Users ---") - user2 = service.register_user( - email="bob@example.com", - username="bob", - full_name="Bob Johnson", - metadata={'role': 'user', 'department': 'marketing'} - ) + user1 = service.register_user( + email="alice@example.com", + username="alice", + full_name="Alice Smith", + metadata={'role': 'admin', 'department': 'engineering'} + ) - user3 = service.register_user( - email="charlie@example.com", - username="charlie" - ) + user2 = service.register_user( + email="bob@example.com", + username="bob", + full_name="Bob Johnson", + metadata={'role': 'user', 'department': 'marketing'} + ) - # List all users - print("\n--- Listing All Users ---") - users = service.list_all_users() - print(f"Total users: {len(users)}") - for user in users: - status = "Active" if user.is_active else "Inactive" - print(f" - {user.username} ({user.email}) - {status}") - - # Update a user - if user1: - print("\n--- Updating User Profile ---") - service.update_user_profile( - user1.user_id, - full_name="Alice M. Smith", - metadata={'role': 'admin', 'department': 'engineering', 'title': 'Senior Engineer'} + user3 = service.register_user( + email="charlie@example.com", + username="charlie" ) - # Deactivate a user - if user3: - print("\n--- Deactivating User ---") - service.deactivate_user(user3.user_id, reason="User requested account suspension") - - # List active users only - print("\n--- Listing Active Users ---") - active_users = service.list_all_users(active_only=True) - print(f"Active users: {len(active_users)}") - for user in active_users: - print(f" - {user.username} ({user.email})") - - # Export users - print("\n--- Exporting Users ---") - exported = service.export_users() - print(f"Exported {len(exported)} users") - - # Shutdown - print("\n--- Shutting Down Service ---") - service.shutdown() - print("Service stopped.") + # List all users + print("\n--- Listing All Users ---") + users = service.list_all_users() + print(f"Total users: {len(users)}") + for user in users: + status = "Active" if user.is_active else "Inactive" + print(f" - {user.username} ({user.email}) - {status}") + + # Update a user + if user1: + print("\n--- Updating User Profile ---") + service.update_user_profile( + user1.user_id, + full_name="Alice M. Smith", + metadata={'role': 'admin', 'department': 'engineering', 'title': 'Senior Engineer'} + ) + + # Deactivate a user + if user3: + print("\n--- Deactivating User ---") + service.deactivate_user(user3.user_id, reason="User requested account suspension") + + # List active users only + print("\n--- Listing Active Users ---") + active_users = service.list_all_users(active_only=True) + print(f"Active users: {len(active_users)}") + for user in active_users: + print(f" - {user.username} ({user.email})") + + # Export users + print("\n--- Exporting Users ---") + exported = service.export_users() + print(f"Exported {len(exported)} users") + + except Exception as error: + if service: + service._capture_exception(error, properties={'service': 'user_management'}) + raise + finally: + # Shutdown + print("\n--- Shutting Down Service ---") + if service: + service.shutdown() + print("Service stopped.") if __name__ == "__main__":