From ab1f65e02e94e152bed1654026f61022abeb6728 Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Mon, 19 Jan 2026 20:31:28 -0500 Subject: [PATCH 01/10] flask example app with PostHog basics --- basics/flask/.env.example | 5 + basics/flask/.gitignore | 7 + basics/flask/README.md | 140 +++++++++++++++++++ basics/flask/app/__init__.py | 83 +++++++++++ basics/flask/app/api/__init__.py | 7 + basics/flask/app/api/routes.py | 85 ++++++++++++ basics/flask/app/config.py | 40 ++++++ basics/flask/app/extensions.py | 10 ++ basics/flask/app/main/__init__.py | 7 + basics/flask/app/main/routes.py | 149 ++++++++++++++++++++ basics/flask/app/models.py | 59 ++++++++ basics/flask/app/templates/base.html | 162 ++++++++++++++++++++++ basics/flask/app/templates/burrito.html | 49 +++++++ basics/flask/app/templates/dashboard.html | 45 ++++++ basics/flask/app/templates/home.html | 38 +++++ basics/flask/app/templates/profile.html | 87 ++++++++++++ basics/flask/app/templates/signup.html | 49 +++++++ basics/flask/requirements.txt | 6 + basics/flask/run.py | 8 ++ 19 files changed, 1036 insertions(+) create mode 100644 basics/flask/.env.example create mode 100644 basics/flask/.gitignore create mode 100644 basics/flask/README.md create mode 100644 basics/flask/app/__init__.py create mode 100644 basics/flask/app/api/__init__.py create mode 100644 basics/flask/app/api/routes.py create mode 100644 basics/flask/app/config.py create mode 100644 basics/flask/app/extensions.py create mode 100644 basics/flask/app/main/__init__.py create mode 100644 basics/flask/app/main/routes.py create mode 100644 basics/flask/app/models.py create mode 100644 basics/flask/app/templates/base.html create mode 100644 basics/flask/app/templates/burrito.html create mode 100644 basics/flask/app/templates/dashboard.html create mode 100644 basics/flask/app/templates/home.html create mode 100644 basics/flask/app/templates/profile.html create mode 100644 basics/flask/app/templates/signup.html create mode 100644 basics/flask/requirements.txt create mode 100644 basics/flask/run.py diff --git a/basics/flask/.env.example b/basics/flask/.env.example new file mode 100644 index 0000000..dc88d83 --- /dev/null +++ b/basics/flask/.env.example @@ -0,0 +1,5 @@ +POSTHOG_API_KEY= +POSTHOG_HOST=https://us.i.posthog.com +FLASK_SECRET_KEY=your-secret-key-here +FLASK_DEBUG=True +POSTHOG_DISABLED=False diff --git a/basics/flask/.gitignore b/basics/flask/.gitignore new file mode 100644 index 0000000..9b010e7 --- /dev/null +++ b/basics/flask/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.py[cod] +.env +*.db +.venv/ +venv/ +instance/ diff --git a/basics/flask/README.md b/basics/flask/README.md new file mode 100644 index 0000000..b39b696 --- /dev/null +++ b/basics/flask/README.md @@ -0,0 +1,140 @@ +# PostHog Flask Example + +A Flask application demonstrating PostHog integration for analytics, feature flags, and error tracking. + +## Features + +- User registration and authentication with Flask-Login +- SQLite database persistence with Flask-SQLAlchemy +- User identification and property tracking +- Custom event tracking +- Feature flags with payload support +- Error tracking and exception capture +- Group analytics + +## Quick Start + +1. Create and activate a virtual environment: + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Copy the environment file and configure: + ```bash + cp .env.example .env + # Edit .env with your PostHog project key + ``` + +4. Run the application: + ```bash + python run.py + ``` + +5. Open http://localhost:5001 and either: + - Login with default credentials: `admin@example.com` / `admin` + - Or click "Sign up here" to create a new account + +## PostHog Integration Points + +### User Registration +New users are identified and tracked on signup using the context-based API: +```python +with new_context(): + identify_context(user.email) + tag('email', user.email) + tag('is_staff', user.is_staff) + capture('user_signed_up', properties={'signup_method': 'form'}) +``` + +### User Identification +Users are identified on login with their properties: +```python +with new_context(): + identify_context(user.email) + tag('email', user.email) + tag('is_staff', user.is_staff) + capture('user_logged_in', properties={'login_method': 'password'}) +``` + +### Event Tracking +Custom events are captured throughout the app: +```python +with new_context(): + identify_context(current_user.email) + capture('burrito_considered', properties={'total_considerations': count}) +``` + +### Feature Flags +The dashboard demonstrates feature flag checking: +```python +show_new_feature = posthog.feature_enabled( + 'new-dashboard-feature', + current_user.email, + person_properties={'email': current_user.email, 'is_staff': current_user.is_staff} +) +feature_config = posthog.get_feature_flag_payload('new-dashboard-feature', current_user.email) +``` + +### Error Tracking +Exceptions are captured for monitoring: +```python +posthog.capture_exception(exception) +``` + +### Group Analytics +Company-level analytics tracking: +```python +posthog.group_identify('company', 'acme-corp', { + 'name': 'Acme Corporation', + 'plan': 'enterprise' +}) + +with new_context(): + identify_context(current_user.email) + capture('feature_used', properties={'feature_name': 'analytics'}, + groups={'company': 'acme-corp'}) +``` + +## Project Structure + +``` +basics/flask/ +├── app/ +│ ├── __init__.py # Application factory +│ ├── config.py # Configuration classes +│ ├── extensions.py # Extension instances +│ ├── models.py # User model (SQLAlchemy) +│ ├── main/ +│ │ ├── __init__.py # Main blueprint +│ │ └── routes.py # View functions +│ ├── templates/ # HTML templates +│ └── api/ +│ ├── __init__.py # API blueprint +│ └── routes.py # API endpoints +├── .env.example +├── .gitignore +├── requirements.txt +├── README.md +└── run.py # Entry point +``` + +## Key Differences from Django Version + +| Aspect | Django | Flask | +|--------|--------|-------| +| Project Structure | Single app in project | Application factory + blueprints | +| Database | SQLite via Django ORM | SQLite via Flask-SQLAlchemy | +| User Model | Built-in `auth.User` model | Custom SQLAlchemy `User` model | +| User Registration | Django admin / `createsuperuser` | `/signup` route with form | +| Authentication | Django auth system | Flask-Login | +| Session Management | Django sessions | Flask sessions (cookie-based) | +| Configuration | settings.py | Config classes with app factory | +| URL Routing | urls.py patterns | Blueprint route decorators | +| PostHog Init | AppConfig.ready() | Application factory | +| Error Capture | PostHog middleware auto-captures | Requires global `@app.errorhandler(Exception)` | diff --git a/basics/flask/app/__init__.py b/basics/flask/app/__init__.py new file mode 100644 index 0000000..65e85bd --- /dev/null +++ b/basics/flask/app/__init__.py @@ -0,0 +1,83 @@ +"""Flask application factory.""" + +import posthog +from flask import Flask, jsonify +from flask_login import current_user +from posthog import identify_context, new_context + +from app.config import config +from app.extensions import db, login_manager + + +def create_app(config_name="default"): + """Application factory.""" + app = Flask(__name__) + app.config.from_object(config[config_name]) + + # Initialize extensions + db.init_app(app) + login_manager.init_app(app) + + # Initialize PostHog + if not app.config["POSTHOG_DISABLED"]: + posthog.api_key = app.config["POSTHOG_API_KEY"] + posthog.host = app.config["POSTHOG_HOST"] + posthog.debug = app.config["DEBUG"] + + # Import models after db is initialized + from app.models import User + + # User loader for Flask-Login + @login_manager.user_loader + def load_user(user_id): + return User.get_by_id(user_id) + + # Global error handler for PostHog exception capture + # Flask's built-in error handlers bypass PostHog's default autocapture, + # so we need to manually capture exceptions here + @app.errorhandler(Exception) + def handle_exception(e): + # Capture the exception in PostHog and get the event UUID + # This UUID can be shown to users for bug reports + # Use context to attribute exception to the current user + with new_context(): + if current_user.is_authenticated: + identify_context(current_user.email) + event_id = posthog.capture_exception(e) + + # For API routes, return JSON error response + if hasattr(e, "code"): + status_code = e.code + else: + status_code = 500 + + return ( + jsonify( + { + "error": str(e), + "error_id": event_id, + "message": "An error occurred. Reference ID: " + + (event_id or "unknown"), + } + ), + status_code, + ) + + # Register blueprints + from app.api import api_bp + from app.main import main_bp + + app.register_blueprint(main_bp) + app.register_blueprint(api_bp, url_prefix="/api") + + # Create database tables and seed default admin user + with app.app_context(): + db.create_all() + if not User.get_by_email("admin@example.com"): + User.create_user( + email="admin@example.com", + password="admin", + is_staff=True, + ) + + return app diff --git a/basics/flask/app/api/__init__.py b/basics/flask/app/api/__init__.py new file mode 100644 index 0000000..eae8f4f --- /dev/null +++ b/basics/flask/app/api/__init__.py @@ -0,0 +1,7 @@ +"""API blueprint registration.""" + +from flask import Blueprint + +api_bp = Blueprint("api", __name__) + +from app.api import routes # noqa: E402, F401 diff --git a/basics/flask/app/api/routes.py b/basics/flask/app/api/routes.py new file mode 100644 index 0000000..287a9fa --- /dev/null +++ b/basics/flask/app/api/routes.py @@ -0,0 +1,85 @@ +"""API endpoints demonstrating PostHog integration patterns.""" + +import posthog +from flask import jsonify, request, session +from flask_login import current_user, login_required +from posthog import capture, identify_context, new_context + +from app.api import api_bp + + +@api_bp.route("/burrito/consider", methods=["POST"]) +@login_required +def consider_burrito(): + """Track burrito consideration event.""" + # Increment session counter + burrito_count = session.get("burrito_count", 0) + 1 + session["burrito_count"] = burrito_count + + # PostHog: Capture custom event + with new_context(): + identify_context(current_user.email) + capture("burrito_considered", properties={"total_considerations": burrito_count}) + + return jsonify({"success": True, "count": burrito_count}) + + +@api_bp.route("/trigger-error", methods=["POST"]) +@login_required +def trigger_error(): + """Demonstrate error tracking.""" + error_type = request.form.get("error_type", "generic") + + try: + if error_type == "value": + raise ValueError("Invalid value provided by user") + elif error_type == "key": + data = {} + _ = data["nonexistent_key"] + else: + raise Exception("A generic error occurred") + except Exception as e: + # PostHog: Capture exception and track error event with user context + with new_context(): + identify_context(current_user.email) + posthog.capture_exception(e) + capture( + "error_triggered", + properties={"error_type": error_type, "error_message": str(e)}, + ) + + return ( + jsonify( + { + "success": False, + "error": str(e), + "message": "Error has been captured by PostHog", + } + ), + 400, + ) + + return jsonify({"success": True}) + + +@api_bp.route("/group-analytics", methods=["GET", "POST"]) +@login_required +def group_analytics(): + """Demonstrate group analytics.""" + # PostHog: Group identify + posthog.group_identify( + "company", + "acme-corp", + {"name": "Acme Corporation", "plan": "enterprise", "employee_count": 500}, + ) + + # Capture event with group association + with new_context(): + identify_context(current_user.email) + capture( + "feature_used", + properties={"feature_name": "group_analytics"}, + groups={"company": "acme-corp"}, + ) + + return jsonify({"success": True, "message": "Group analytics event captured"}) diff --git a/basics/flask/app/config.py b/basics/flask/app/config.py new file mode 100644 index 0000000..452e6d3 --- /dev/null +++ b/basics/flask/app/config.py @@ -0,0 +1,40 @@ +"""Flask application configuration.""" + +import os +from dotenv import load_dotenv + +load_dotenv() + + +class Config: + """Base configuration.""" + + SECRET_KEY = os.environ.get("FLASK_SECRET_KEY", "dev-secret-key-change-in-production") + + # Database configuration (SQLite like Django example) + SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///db.sqlite3") + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # PostHog configuration + POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", "") + POSTHOG_HOST = os.environ.get("POSTHOG_HOST", "https://us.i.posthog.com") + POSTHOG_DISABLED = os.environ.get("POSTHOG_DISABLED", "False").lower() == "true" + + +class DevelopmentConfig(Config): + """Development configuration.""" + + DEBUG = True + + +class ProductionConfig(Config): + """Production configuration.""" + + DEBUG = False + + +config = { + "development": DevelopmentConfig, + "production": ProductionConfig, + "default": DevelopmentConfig, +} diff --git a/basics/flask/app/extensions.py b/basics/flask/app/extensions.py new file mode 100644 index 0000000..06a84d6 --- /dev/null +++ b/basics/flask/app/extensions.py @@ -0,0 +1,10 @@ +"""Flask extensions initialized without binding to app.""" + +from flask_login import LoginManager +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +login_manager = LoginManager() +login_manager.login_view = "core.home" +login_manager.login_message = "Please log in to access this page." diff --git a/basics/flask/app/main/__init__.py b/basics/flask/app/main/__init__.py new file mode 100644 index 0000000..d9d9053 --- /dev/null +++ b/basics/flask/app/main/__init__.py @@ -0,0 +1,7 @@ +"""Main blueprint registration.""" + +from flask import Blueprint + +main_bp = Blueprint("main", __name__, template_folder="../templates") + +from app.main import routes # noqa: E402, F401 diff --git a/basics/flask/app/main/routes.py b/basics/flask/app/main/routes.py new file mode 100644 index 0000000..e5a3ab1 --- /dev/null +++ b/basics/flask/app/main/routes.py @@ -0,0 +1,149 @@ +"""Core view functions demonstrating PostHog integration patterns.""" + +import posthog +from flask import flash, redirect, render_template, request, session, url_for +from flask_login import current_user, login_required, login_user, logout_user +from posthog import capture, identify_context, new_context, tag + +from app.main import main_bp +from app.models import User + + +@main_bp.route("/", methods=["GET", "POST"]) +def home(): + """Home/login page.""" + if current_user.is_authenticated: + return redirect(url_for("main.dashboard")) + + if request.method == "POST": + email = request.form.get("email") + password = request.form.get("password") + + user = User.authenticate(email, password) + if user: + login_user(user) + + # PostHog: Identify user and capture login event + with new_context(): + identify_context(user.email) + + # Set person properties (PII goes in tag, not capture) + tag("email", user.email) + tag("is_staff", user.is_staff) + tag("date_joined", user.date_joined.isoformat()) + + capture("user_logged_in", properties={"login_method": "password"}) + + return redirect(url_for("main.dashboard")) + else: + flash("Invalid email or password", "error") + + return render_template("home.html") + + +@main_bp.route("/signup", methods=["GET", "POST"]) +def signup(): + """User registration page.""" + if current_user.is_authenticated: + return redirect(url_for("main.dashboard")) + + if request.method == "POST": + email = request.form.get("email") + password = request.form.get("password") + password_confirm = request.form.get("password_confirm") + + # Validation + if not email or not password: + flash("Email and password are required", "error") + elif password != password_confirm: + flash("Passwords do not match", "error") + elif User.get_by_email(email): + flash("Email already registered", "error") + else: + # Create new user + user = User.create_user( + email=email, + password=password, + is_staff=False, + ) + + # PostHog: Identify new user and capture signup event + with new_context(): + identify_context(user.email) + + tag("email", user.email) + tag("is_staff", user.is_staff) + tag("date_joined", user.date_joined.isoformat()) + + capture("user_signed_up", properties={"signup_method": "form"}) + + # Log the user in + login_user(user) + flash("Account created successfully!", "success") + return redirect(url_for("main.dashboard")) + + return render_template("signup.html") + + +@main_bp.route("/logout") +@login_required +def logout(): + """Logout and capture event.""" + # PostHog: Capture logout event before session ends + with new_context(): + identify_context(current_user.email) + capture("user_logged_out") + + logout_user() + return redirect(url_for("main.home")) + + +@main_bp.route("/dashboard") +@login_required +def dashboard(): + """Dashboard with feature flag demonstration.""" + # PostHog: Capture dashboard view + with new_context(): + identify_context(current_user.email) + capture("dashboard_viewed", properties={"is_staff": current_user.is_staff}) + + # Check feature flag + show_new_feature = posthog.feature_enabled( + "new-dashboard-feature", + current_user.email, + person_properties={ + "email": current_user.email, + "is_staff": current_user.is_staff, + }, + ) + + # Get feature flag payload + feature_config = posthog.get_feature_flag_payload( + "new-dashboard-feature", current_user.email + ) + + return render_template( + "dashboard.html", + show_new_feature=show_new_feature, + feature_config=feature_config, + ) + + +@main_bp.route("/burrito") +@login_required +def burrito(): + """Burrito consideration tracker page.""" + burrito_count = session.get("burrito_count", 0) + return render_template("burrito.html", burrito_count=burrito_count) + + +@main_bp.route("/profile") +@login_required +def profile(): + """User profile page.""" + # PostHog: Capture profile view + with new_context(): + identify_context(current_user.email) + capture("profile_viewed") + + return render_template("profile.html") diff --git a/basics/flask/app/models.py b/basics/flask/app/models.py new file mode 100644 index 0000000..48e9a77 --- /dev/null +++ b/basics/flask/app/models.py @@ -0,0 +1,59 @@ +"""User model with SQLite persistence (similar to Django's auth.User).""" + +from datetime import datetime, timezone + +from flask_login import UserMixin +from werkzeug.security import check_password_hash, generate_password_hash + +from app.extensions import db + + +class User(UserMixin, db.Model): + """User model with SQLite persistence.""" + + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(254), unique=True, nullable=False) + password_hash = db.Column(db.String(256), nullable=False) + is_staff = db.Column(db.Boolean, default=False) + is_active = db.Column(db.Boolean, default=True) + date_joined = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + def set_password(self, password): + """Hash and set the user's password.""" + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + """Verify the password against the hash.""" + return check_password_hash(self.password_hash, password) + + @classmethod + def create_user(cls, email, password, is_staff=False): + """Create and save a new user.""" + user = cls(email=email, is_staff=is_staff) + user.set_password(password) + db.session.add(user) + db.session.commit() + return user + + @classmethod + def get_by_id(cls, user_id): + """Get user by ID.""" + return cls.query.get(int(user_id)) + + @classmethod + def get_by_email(cls, email): + """Get user by email.""" + return cls.query.filter_by(email=email).first() + + @classmethod + def authenticate(cls, email, password): + """Authenticate user with email and password.""" + user = cls.get_by_email(email) + if user and user.check_password(password): + return user + return None + + def __repr__(self): + return f"" diff --git a/basics/flask/app/templates/base.html b/basics/flask/app/templates/base.html new file mode 100644 index 0000000..9a54975 --- /dev/null +++ b/basics/flask/app/templates/base.html @@ -0,0 +1,162 @@ + + + + + + {% block title %}PostHog Flask Example{% endblock %} + + + + {% if current_user.is_authenticated %} + + {% endif %} + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + {% block scripts %}{% endblock %} + + diff --git a/basics/flask/app/templates/burrito.html b/basics/flask/app/templates/burrito.html new file mode 100644 index 0000000..0ce4220 --- /dev/null +++ b/basics/flask/app/templates/burrito.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block title %}Burrito - PostHog Flask Example{% endblock %} + +{% block content %} +
+

Burrito Consideration Tracker

+

This page demonstrates custom event tracking with PostHog.

+ +
{{ burrito_count }}
+

Times you've considered a burrito

+ +
+ +
+
+ +
+

Code Example

+
+# API endpoint captures the event
+with new_context():
+    identify_context(current_user.email)
+    capture('burrito_considered', properties={
+        'total_considerations': burrito_count
+    })
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/basics/flask/app/templates/dashboard.html b/basics/flask/app/templates/dashboard.html new file mode 100644 index 0000000..e27c93f --- /dev/null +++ b/basics/flask/app/templates/dashboard.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block title %}Dashboard - PostHog Flask Example{% endblock %} + +{% block content %} +
+

Dashboard

+

Welcome back, {{ current_user.username }}!

+
+ +
+

Feature Flags

+ + {% if show_new_feature %} +
+ New Feature Enabled! +

You're seeing this because the new-dashboard-feature flag is enabled for you.

+ {% if feature_config %} +

Feature Configuration:

+
{{ feature_config | tojson(indent=2) }}
+ {% endif %} +
+ {% else %} +

The new-dashboard-feature flag is not enabled for your account.

+ {% endif %} + +

Code Example

+
+# Check if feature flag is enabled
+show_new_feature = posthog.feature_enabled(
+    'new-dashboard-feature',
+    user_id,
+    person_properties={
+        'email': current_user.email,
+        'is_staff': current_user.is_staff
+    }
+)
+
+# Get feature flag payload
+feature_config = posthog.get_feature_flag_payload(
+    'new-dashboard-feature',
+    user_id
+)
+
+{% endblock %} diff --git a/basics/flask/app/templates/home.html b/basics/flask/app/templates/home.html new file mode 100644 index 0000000..21a598a --- /dev/null +++ b/basics/flask/app/templates/home.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% block title %}Login - PostHog Flask Example{% endblock %} + +{% block content %} +
+

Welcome to PostHog Flask Example

+

This example demonstrates how to integrate PostHog with a Flask application.

+ +
+ + + + + + + +
+ +

+ Don't have an account? Sign up here +

+

+ Tip: Default credentials are admin@example.com/admin +

+
+ +
+

Features Demonstrated

+
    +
  • User registration and identification
  • +
  • Event tracking
  • +
  • Feature flags
  • +
  • Error tracking
  • +
  • Group analytics
  • +
+
+{% endblock %} diff --git a/basics/flask/app/templates/profile.html b/basics/flask/app/templates/profile.html new file mode 100644 index 0000000..cc5dbae --- /dev/null +++ b/basics/flask/app/templates/profile.html @@ -0,0 +1,87 @@ +{% extends "base.html" %} + +{% block title %}Profile - PostHog Flask Example{% endblock %} + +{% block content %} +
+

Your Profile

+

This page demonstrates error tracking with PostHog.

+ + + + + + + + + + + + + + +
Email{{ current_user.email }}
Date Joined{{ current_user.date_joined.strftime('%Y-%m-%d %H:%M') }}
Staff Status{{ 'Yes' if current_user.is_staff else 'No' }}
+
+ +
+

Error Tracking Demo

+

Click a button to trigger an error and see it captured in PostHog:

+ +
+ + + +
+ + +
+ +
+

Code Example

+
+try:
+    raise ValueError('Invalid value provided')
+except Exception as e:
+    # Capture exception and event with user context
+    with new_context():
+        identify_context(current_user.email)
+        posthog.capture_exception(e)
+        capture('error_triggered', properties={
+            'error_type': 'value',
+            'error_message': str(e)
+        })
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/basics/flask/app/templates/signup.html b/basics/flask/app/templates/signup.html new file mode 100644 index 0000000..085a6c3 --- /dev/null +++ b/basics/flask/app/templates/signup.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block title %}Sign Up - PostHog Flask Example{% endblock %} + +{% block content %} +
+

Create an Account

+

Sign up to explore the PostHog Flask integration example.

+ +
+ + + + + + + + + + +
+ +

+ Already have an account? Login here +

+
+ +
+

PostHog Integration

+

When you sign up, the following PostHog events are captured:

+
    +
  • identify_context() - Associates your email with the context
  • +
  • tag() - Sets person properties (email, etc.)
  • +
  • user_signed_up event - Tracks the signup action
  • +
+ +

Code Example

+
+# After creating the user
+with new_context():
+    identify_context(user.email)
+
+    tag('email', user.email)
+    tag('is_staff', user.is_staff)
+    tag('date_joined', user.date_joined.isoformat())
+
+    capture('user_signed_up', properties={'signup_method': 'form'})
+
+{% endblock %} diff --git a/basics/flask/requirements.txt b/basics/flask/requirements.txt new file mode 100644 index 0000000..b27e078 --- /dev/null +++ b/basics/flask/requirements.txt @@ -0,0 +1,6 @@ +Flask>=3.1.0 +Flask-Login>=0.6.3 +Flask-SQLAlchemy>=3.1.0 +python-dotenv>=1.0.0 +posthog>=3.0.0 +Werkzeug>=3.0.0 diff --git a/basics/flask/run.py b/basics/flask/run.py new file mode 100644 index 0000000..d7d8d8b --- /dev/null +++ b/basics/flask/run.py @@ -0,0 +1,8 @@ +"""Development server entry point.""" + +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True, port=5001) From 25702240d136d1027a2da73c81ed3f0b27f0be47 Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Mon, 19 Jan 2026 20:45:32 -0500 Subject: [PATCH 02/10] CI suggestions --- basics/flask/app/api/routes.py | 2 +- basics/flask/app/models.py | 1 + basics/flask/run.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/basics/flask/app/api/routes.py b/basics/flask/app/api/routes.py index 287a9fa..b22818d 100644 --- a/basics/flask/app/api/routes.py +++ b/basics/flask/app/api/routes.py @@ -52,7 +52,7 @@ def trigger_error(): jsonify( { "success": False, - "error": str(e), + "error": "An error occurred", "message": "Error has been captured by PostHog", } ), diff --git a/basics/flask/app/models.py b/basics/flask/app/models.py index 48e9a77..5797e8c 100644 --- a/basics/flask/app/models.py +++ b/basics/flask/app/models.py @@ -32,6 +32,7 @@ def check_password(self, password): def create_user(cls, email, password, is_staff=False): """Create and save a new user.""" user = cls(email=email, is_staff=is_staff) + # nosemgrep: python.django.security.audit.unvalidated-password.unvalidated-password user.set_password(password) db.session.add(user) db.session.commit() diff --git a/basics/flask/run.py b/basics/flask/run.py index d7d8d8b..e7a7e3e 100644 --- a/basics/flask/run.py +++ b/basics/flask/run.py @@ -5,4 +5,4 @@ app = create_app() if __name__ == "__main__": - app.run(debug=True, port=5001) + app.run(port=5001) From b4d5288e7fe6e56082dde768b3fb64172bed37ca Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Mon, 19 Jan 2026 20:57:40 -0500 Subject: [PATCH 03/10] flask skill creation --- scripts/build-examples-mcp-resources.js | 21 +++++++++++++++++++++ transformation-config/skills.yaml | 9 +++++++++ transformation-config/skip-patterns.yaml | 6 ++++++ 3 files changed, 36 insertions(+) diff --git a/scripts/build-examples-mcp-resources.js b/scripts/build-examples-mcp-resources.js index 5f9fef8..baaaffc 100644 --- a/scripts/build-examples-mcp-resources.js +++ b/scripts/build-examples-mcp-resources.js @@ -204,6 +204,27 @@ const defaultConfig = { regex: [], }, plugins: [], + }, + { + path: 'basics/flask', + id: 'flask', + displayName: 'Flask', + tags: ['flask', 'python', 'server-side'], + skipPatterns: { + includes: [ + '__pycache__', + '.pyc', + '.pyo', + '.pyd', + '.env', + '.db', + '.venv', + 'venv', + 'instance', + ], + regex: [], + }, + plugins: [], } ], globalSkipPatterns: { diff --git a/transformation-config/skills.yaml b/transformation-config/skills.yaml index 1f6efe6..aba6831 100644 --- a/transformation-config/skills.yaml +++ b/transformation-config/skills.yaml @@ -70,6 +70,15 @@ skills: docs_urls: - https://posthog.com/docs/libraries/django.md + - id: flask + type: example + example_path: basics/flask + display_name: Flask + description: PostHog integration for Flask applications + tags: [flask, python] + docs_urls: + - https://posthog.com/docs/libraries/flask.md + # Guide-only skills (docs without example code) # - id: identify-users # type: guide diff --git a/transformation-config/skip-patterns.yaml b/transformation-config/skip-patterns.yaml index c3ca766..c43d28d 100644 --- a/transformation-config/skip-patterns.yaml +++ b/transformation-config/skip-patterns.yaml @@ -63,6 +63,8 @@ global: - .venv - __pycache__ - .pyc + - .pyo + - .pyd - .egg-info - .eggs - .pytest_cache @@ -75,6 +77,10 @@ global: - pip-log.txt - .Python + # Flask + - instance + - .db + # Regex patterns - skip if path matches # Note: Patterns are JavaScript regex syntax regex: From 5db66f362317a0001fa9cf7d4fa44ebbda29162c Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Tue, 20 Jan 2026 19:51:26 -0500 Subject: [PATCH 04/10] better flask error handling and template pages --- basics/flask/app/__init__.py | 57 ++++++++++++++-------- basics/flask/app/api/routes.py | 56 +++++++++------------ basics/flask/app/extensions.py | 2 +- basics/flask/app/templates/errors/404.html | 30 ++++++++++++ basics/flask/app/templates/errors/500.html | 37 ++++++++++++++ 5 files changed, 127 insertions(+), 55 deletions(-) create mode 100644 basics/flask/app/templates/errors/404.html create mode 100644 basics/flask/app/templates/errors/500.html diff --git a/basics/flask/app/__init__.py b/basics/flask/app/__init__.py index 65e85bd..792096a 100644 --- a/basics/flask/app/__init__.py +++ b/basics/flask/app/__init__.py @@ -1,9 +1,10 @@ """Flask application factory.""" import posthog -from flask import Flask, jsonify +from flask import Flask, g, jsonify, render_template, request from flask_login import current_user from posthog import identify_context, new_context +from werkzeug.exceptions import HTTPException from app.config import config from app.extensions import db, login_manager @@ -33,35 +34,49 @@ def load_user(user_id): return User.get_by_id(user_id) # Global error handler for PostHog exception capture - # Flask's built-in error handlers bypass PostHog's default autocapture, - # so we need to manually capture exceptions here + # Production best practice: Only capture server errors (5xx), not client errors (4xx) @app.errorhandler(Exception) def handle_exception(e): - # Capture the exception in PostHog and get the event UUID - # This UUID can be shown to users for bug reports - # Use context to attribute exception to the current user + # Skip capturing client errors (4xx) - these are user issues, not bugs + # Only capture server errors (5xx) and unhandled exceptions + event_id = None + if isinstance(e, HTTPException): + if e.code < 500: + # Client error (4xx) - don't capture, just handle normally + if request.path.startswith('/api/'): + return jsonify({"error": e.description}), e.code + return e.get_response() + + # Server error (5xx) or unhandled exception - capture in PostHog with new_context(): if current_user.is_authenticated: identify_context(current_user.email) event_id = posthog.capture_exception(e) # For API routes, return JSON error response - if hasattr(e, "code"): - status_code = e.code - else: - status_code = 500 - - return ( - jsonify( - { - "error": str(e), + if request.path.startswith('/api/'): + if isinstance(e, HTTPException): + return jsonify({ + "error": e.description, "error_id": event_id, - "message": "An error occurred. Reference ID: " - + (event_id or "unknown"), - } - ), - status_code, - ) + "message": f"An error occurred. Reference ID: {event_id}" + }), e.code + return jsonify({ + "error": "Internal server error", + "error_id": event_id, + "message": f"An error occurred. Reference ID: {event_id}" + }), 500 + + # For web routes, render 500 error page with error_id + # (All 5xx HTTPExceptions reach here; 4xx are handled above) + return render_template('errors/500.html', error_id=event_id, error=str(e) if not isinstance(e, HTTPException) else None), 500 + + # Specific handler for 404 - no PostHog capture + @app.errorhandler(404) + def page_not_found(e): + if request.path.startswith('/api/'): + return jsonify({"error": "Not found"}), 404 + return render_template('errors/404.html'), 404 # Register blueprints from app.api import api_bp diff --git a/basics/flask/app/api/routes.py b/basics/flask/app/api/routes.py index b22818d..d9fbb2a 100644 --- a/basics/flask/app/api/routes.py +++ b/basics/flask/app/api/routes.py @@ -24,40 +24,30 @@ def consider_burrito(): return jsonify({"success": True, "count": burrito_count}) -@api_bp.route("/trigger-error", methods=["POST"]) +@api_bp.route("/test-error", methods=["POST"]) @login_required -def trigger_error(): - """Demonstrate error tracking.""" - error_type = request.form.get("error_type", "generic") - - try: - if error_type == "value": - raise ValueError("Invalid value provided by user") - elif error_type == "key": - data = {} - _ = data["nonexistent_key"] - else: - raise Exception("A generic error occurred") - except Exception as e: - # PostHog: Capture exception and track error event with user context - with new_context(): - identify_context(current_user.email) - posthog.capture_exception(e) - capture( - "error_triggered", - properties={"error_type": error_type, "error_message": str(e)}, - ) - - return ( - jsonify( - { - "success": False, - "error": "An error occurred", - "message": "Error has been captured by PostHog", - } - ), - 400, - ) +def test_error(): + """Test endpoint to manually trigger errors for PostHog error tracking verification. + + Only server errors (5xx) are captured in PostHog. + Client errors (4xx) are not captured as they represent user issues, not bugs. + + Query params: + - error_type: "server" (500), "client" (400), "not_found" (404) + """ + error_type = request.args.get("error_type", "server") + + if error_type == "client": + # Client error (400) - NOT captured in PostHog + from werkzeug.exceptions import BadRequest + raise BadRequest("Invalid request - this is a client error and won't be captured") + elif error_type == "not_found": + # 404 error - NOT captured in PostHog + from werkzeug.exceptions import NotFound + raise NotFound("Resource not found - this won't be captured") + else: + # Server error (500) - WILL be captured in PostHog + raise Exception("Test server error - this WILL be captured in PostHog") return jsonify({"success": True}) diff --git a/basics/flask/app/extensions.py b/basics/flask/app/extensions.py index 06a84d6..4366575 100644 --- a/basics/flask/app/extensions.py +++ b/basics/flask/app/extensions.py @@ -6,5 +6,5 @@ db = SQLAlchemy() login_manager = LoginManager() -login_manager.login_view = "core.home" +login_manager.login_view = "main.home" login_manager.login_message = "Please log in to access this page." diff --git a/basics/flask/app/templates/errors/404.html b/basics/flask/app/templates/errors/404.html new file mode 100644 index 0000000..2b34c1a --- /dev/null +++ b/basics/flask/app/templates/errors/404.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block title %}404 - Page Not Found{% endblock %} + +{% block content %} +
+

404

+

Page Not Found

+

+ The page you're looking for doesn't exist or has been moved. +

+ + {% if error_id %} +
+

Error Reference ID:

+ {{ error_id }} +

+ Share this ID with support if you need assistance. +

+
+ {% endif %} + +
+ Go to Home + {% if current_user.is_authenticated %} + Go to Dashboard + {% endif %} +
+
+{% endblock %} diff --git a/basics/flask/app/templates/errors/500.html b/basics/flask/app/templates/errors/500.html new file mode 100644 index 0000000..c9026a9 --- /dev/null +++ b/basics/flask/app/templates/errors/500.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}500 - Internal Server Error{% endblock %} + +{% block content %} +
+

500

+

Internal Server Error

+

+ Something went wrong on our end. We've been notified and are looking into it. +

+ + {% if error_id %} +
+

Error Reference ID:

+ {{ error_id }} +

+ Share this ID with support if you need assistance. This error has been logged in PostHog. +

+
+ {% endif %} + + {% if error and config.DEBUG %} +
+

Debug Information:

+ {{ error }} +
+ {% endif %} + +
+ Go to Home + {% if current_user.is_authenticated %} + Go to Dashboard + {% endif %} +
+
+{% endblock %} From 9212f9bcb9b7ed5efbcf4c8d4617756598ad0c06 Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Tue, 20 Jan 2026 20:49:17 -0500 Subject: [PATCH 05/10] simplify error handling even more --- basics/flask/app/__init__.py | 49 +++++++--------------------------- basics/flask/app/api/routes.py | 43 ++++++++++++++++------------- 2 files changed, 34 insertions(+), 58 deletions(-) diff --git a/basics/flask/app/__init__.py b/basics/flask/app/__init__.py index 792096a..0111394 100644 --- a/basics/flask/app/__init__.py +++ b/basics/flask/app/__init__.py @@ -33,51 +33,22 @@ def create_app(config_name="default"): def load_user(user_id): return User.get_by_id(user_id) - # Global error handler for PostHog exception capture - # Production best practice: Only capture server errors (5xx), not client errors (4xx) - @app.errorhandler(Exception) - def handle_exception(e): - # Skip capturing client errors (4xx) - these are user issues, not bugs - # Only capture server errors (5xx) and unhandled exceptions - event_id = None - if isinstance(e, HTTPException): - if e.code < 500: - # Client error (4xx) - don't capture, just handle normally - if request.path.startswith('/api/'): - return jsonify({"error": e.description}), e.code - return e.get_response() - - # Server error (5xx) or unhandled exception - capture in PostHog - with new_context(): - if current_user.is_authenticated: - identify_context(current_user.email) - event_id = posthog.capture_exception(e) - - # For API routes, return JSON error response - if request.path.startswith('/api/'): - if isinstance(e, HTTPException): - return jsonify({ - "error": e.description, - "error_id": event_id, - "message": f"An error occurred. Reference ID: {event_id}" - }), e.code - return jsonify({ - "error": "Internal server error", - "error_id": event_id, - "message": f"An error occurred. Reference ID: {event_id}" - }), 500 - - # For web routes, render 500 error page with error_id - # (All 5xx HTTPExceptions reach here; 4xx are handled above) - return render_template('errors/500.html', error_id=event_id, error=str(e) if not isinstance(e, HTTPException) else None), 500 - - # Specific handler for 404 - no PostHog capture + # Simple error handlers - no automatic PostHog capture + # Capture exceptions manually only where it makes sense (e.g., test endpoints) @app.errorhandler(404) def page_not_found(e): if request.path.startswith('/api/'): return jsonify({"error": "Not found"}), 404 return render_template('errors/404.html'), 404 + @app.errorhandler(500) + def internal_server_error(e): + # Capture 500 errors in PostHog - remove this if you want manual control + posthog.capture_exception(e) + if request.path.startswith('/api/'): + return jsonify({"error": "Internal server error"}), 500 + return render_template('errors/500.html'), 500 + # Register blueprints from app.api import api_bp from app.main import main_bp diff --git a/basics/flask/app/api/routes.py b/basics/flask/app/api/routes.py index d9fbb2a..6b3881a 100644 --- a/basics/flask/app/api/routes.py +++ b/basics/flask/app/api/routes.py @@ -27,29 +27,34 @@ def consider_burrito(): @api_bp.route("/test-error", methods=["POST"]) @login_required def test_error(): - """Test endpoint to manually trigger errors for PostHog error tracking verification. + """Test endpoint demonstrating manual exception capture in PostHog. - Only server errors (5xx) are captured in PostHog. - Client errors (4xx) are not captured as they represent user issues, not bugs. + Shows how to intentionally capture specific errors in PostHog. + Use this pattern for critical operations where you want error tracking. Query params: - - error_type: "server" (500), "client" (400), "not_found" (404) + - capture: "true" to capture the exception in PostHog, "false" to just raise it """ - error_type = request.args.get("error_type", "server") - - if error_type == "client": - # Client error (400) - NOT captured in PostHog - from werkzeug.exceptions import BadRequest - raise BadRequest("Invalid request - this is a client error and won't be captured") - elif error_type == "not_found": - # 404 error - NOT captured in PostHog - from werkzeug.exceptions import NotFound - raise NotFound("Resource not found - this won't be captured") - else: - # Server error (500) - WILL be captured in PostHog - raise Exception("Test server error - this WILL be captured in PostHog") - - return jsonify({"success": True}) + should_capture = request.args.get("capture", "true").lower() == "true" + + try: + # Simulate a critical operation failure + raise Exception("Test exception from critical operation") + except Exception as e: + if should_capture: + # Manually capture this specific exception in PostHog + with new_context(): + identify_context(current_user.email) + event_id = posthog.capture_exception(e) + + return jsonify({ + "error": "Operation failed", + "error_id": event_id, + "message": f"Error captured in PostHog. Reference ID: {event_id}" + }), 500 + else: + # Just return error without PostHog capture + return jsonify({"error": str(e)}), 500 @api_bp.route("/group-analytics", methods=["GET", "POST"]) From 932db57a851a515c5c18c56a1f71815a312b8280 Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Tue, 20 Jan 2026 20:59:19 -0500 Subject: [PATCH 06/10] Remove group indentify --- basics/flask/README.md | 46 ++++++++++++++++++++++------------ basics/flask/app/api/routes.py | 21 ---------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/basics/flask/README.md b/basics/flask/README.md index b39b696..a677f2a 100644 --- a/basics/flask/README.md +++ b/basics/flask/README.md @@ -9,8 +9,7 @@ A Flask application demonstrating PostHog integration for analytics, feature fla - User identification and property tracking - Custom event tracking - Feature flags with payload support -- Error tracking and exception capture -- Group analytics +- Error tracking with manual exception capture ## Quick Start @@ -82,25 +81,40 @@ feature_config = posthog.get_feature_flag_payload('new-dashboard-feature', curre ``` ### Error Tracking -Exceptions are captured for monitoring: + +The example demonstrates two approaches to error tracking: + +**1. Automatic capture for all 500 errors** (`app/__init__.py`): ```python -posthog.capture_exception(exception) +@app.errorhandler(500) +def internal_server_error(e): + # Capture 500 errors in PostHog - remove this if you want manual control + posthog.capture_exception(e) + if request.path.startswith('/api/'): + return jsonify({"error": "Internal server error"}), 500 + return render_template('errors/500.html'), 500 ``` -### Group Analytics -Company-level analytics tracking: +**2. Manual capture for specific critical operations** (`app/api/routes.py`): ```python -posthog.group_identify('company', 'acme-corp', { - 'name': 'Acme Corporation', - 'plan': 'enterprise' -}) - -with new_context(): - identify_context(current_user.email) - capture('feature_used', properties={'feature_name': 'analytics'}, - groups={'company': 'acme-corp'}) +try: + # Critical operation that might fail + result = process_payment() +except Exception as e: + # Manually capture this specific exception + with new_context(): + identify_context(current_user.email) + event_id = posthog.capture_exception(e) + + return jsonify({ + "error": "Operation failed", + "error_id": event_id, + "message": f"Error captured in PostHog. Reference ID: {event_id}" + }), 500 ``` +The `/api/test-error` endpoint demonstrates manual exception capture. Use `?capture=true` to capture in PostHog, or `?capture=false` to skip tracking. + ## Project Structure ``` @@ -137,4 +151,4 @@ basics/flask/ | Configuration | settings.py | Config classes with app factory | | URL Routing | urls.py patterns | Blueprint route decorators | | PostHog Init | AppConfig.ready() | Application factory | -| Error Capture | PostHog middleware auto-captures | Requires global `@app.errorhandler(Exception)` | +| Error Capture | PostHog middleware auto-captures | Manual with `@app.errorhandler(500)` or try/except | diff --git a/basics/flask/app/api/routes.py b/basics/flask/app/api/routes.py index 6b3881a..8a24cc5 100644 --- a/basics/flask/app/api/routes.py +++ b/basics/flask/app/api/routes.py @@ -57,24 +57,3 @@ def test_error(): return jsonify({"error": str(e)}), 500 -@api_bp.route("/group-analytics", methods=["GET", "POST"]) -@login_required -def group_analytics(): - """Demonstrate group analytics.""" - # PostHog: Group identify - posthog.group_identify( - "company", - "acme-corp", - {"name": "Acme Corporation", "plan": "enterprise", "employee_count": 500}, - ) - - # Capture event with group association - with new_context(): - identify_context(current_user.email) - capture( - "feature_used", - properties={"feature_name": "group_analytics"}, - groups={"company": "acme-corp"}, - ) - - return jsonify({"success": True, "message": "Group analytics event captured"}) From 6295b0e88070f779e46aab1eedf02bacebbe3c5f Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Tue, 20 Jan 2026 21:27:27 -0500 Subject: [PATCH 07/10] Remove 500 capture --- basics/flask/app/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/basics/flask/app/__init__.py b/basics/flask/app/__init__.py index 0111394..fc74077 100644 --- a/basics/flask/app/__init__.py +++ b/basics/flask/app/__init__.py @@ -43,8 +43,6 @@ def page_not_found(e): @app.errorhandler(500) def internal_server_error(e): - # Capture 500 errors in PostHog - remove this if you want manual control - posthog.capture_exception(e) if request.path.startswith('/api/'): return jsonify({"error": "Internal server error"}), 500 return render_template('errors/500.html'), 500 From c7fa4bd48c5149e184c71dc17614ea22efa97729 Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Tue, 20 Jan 2026 21:30:23 -0500 Subject: [PATCH 08/10] readme --- basics/flask/README.md | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/basics/flask/README.md b/basics/flask/README.md index a677f2a..74f7074 100644 --- a/basics/flask/README.md +++ b/basics/flask/README.md @@ -84,18 +84,8 @@ feature_config = posthog.get_feature_flag_payload('new-dashboard-feature', curre The example demonstrates two approaches to error tracking: -**1. Automatic capture for all 500 errors** (`app/__init__.py`): -```python -@app.errorhandler(500) -def internal_server_error(e): - # Capture 500 errors in PostHog - remove this if you want manual control - posthog.capture_exception(e) - if request.path.startswith('/api/'): - return jsonify({"error": "Internal server error"}), 500 - return render_template('errors/500.html'), 500 -``` +Manual capture for specific critical operations** (`app/api/routes.py`). -**2. Manual capture for specific critical operations** (`app/api/routes.py`): ```python try: # Critical operation that might fail @@ -137,18 +127,3 @@ basics/flask/ ├── README.md └── run.py # Entry point ``` - -## Key Differences from Django Version - -| Aspect | Django | Flask | -|--------|--------|-------| -| Project Structure | Single app in project | Application factory + blueprints | -| Database | SQLite via Django ORM | SQLite via Flask-SQLAlchemy | -| User Model | Built-in `auth.User` model | Custom SQLAlchemy `User` model | -| User Registration | Django admin / `createsuperuser` | `/signup` route with form | -| Authentication | Django auth system | Flask-Login | -| Session Management | Django sessions | Flask sessions (cookie-based) | -| Configuration | settings.py | Config classes with app factory | -| URL Routing | urls.py patterns | Blueprint route decorators | -| PostHog Init | AppConfig.ready() | Application factory | -| Error Capture | PostHog middleware auto-captures | Manual with `@app.errorhandler(500)` or try/except | From d34e9af27ea1242fdb078608da4a9197cae95ba0 Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Tue, 20 Jan 2026 22:21:17 -0500 Subject: [PATCH 09/10] commandments --- transformation-config/commandments.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/transformation-config/commandments.yaml b/transformation-config/commandments.yaml index 4b81f48..972b14b 100644 --- a/transformation-config/commandments.yaml +++ b/transformation-config/commandments.yaml @@ -23,4 +23,8 @@ commandments: - Use the context API pattern with new_context(), identify_context(user_id), then capture() - For login/logout views, create a new context since user state changes during the request - Do NOT create custom middleware, distinct_id helpers, or conditional checks - the SDK handles these - \ No newline at end of file + + flask: + - Initialize PostHog globally in create_app() using posthog.api_key and posthog.host (NOT per-request) + - Manually capture exceptions with `posthog.capture_exception(e)` for error tracking since Flask has built-in error handlers + - Blueprint registration happens AFTER PostHog initialization in create_app() \ No newline at end of file From 9e983de2549c437827e89eef842b9ffc9da1e889 Mon Sep 17 00:00:00 2001 From: edwinyjlim Date: Wed, 21 Jan 2026 14:21:06 -0500 Subject: [PATCH 10/10] python install commandment --- transformation-config/commandments.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/transformation-config/commandments.yaml b/transformation-config/commandments.yaml index 972b14b..0a8ce70 100644 --- a/transformation-config/commandments.yaml +++ b/transformation-config/commandments.yaml @@ -16,6 +16,7 @@ commandments: python: - 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 django: - Add 'posthog.integrations.django.PosthogContextMiddleware' to MIDDLEWARE it auto-extracts tracing headers and captures exceptions