-
Notifications
You must be signed in to change notification settings - Fork 0
flask example app with PostHog basics #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ab1f65e
2570224
b4d5288
ee3f1dc
5db66f3
9212f9b
932db57
6295b0e
c7fa4bd
d34e9af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| POSTHOG_API_KEY=<ph_project_api_key> | ||
| POSTHOG_HOST=https://us.i.posthog.com | ||
| FLASK_SECRET_KEY=your-secret-key-here | ||
| FLASK_DEBUG=True | ||
| POSTHOG_DISABLED=False |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| __pycache__/ | ||
| *.py[cod] | ||
| .env | ||
| *.db | ||
| .venv/ | ||
| venv/ | ||
| instance/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| # 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 with manual exception capture | ||
|
|
||
| ## 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 | ||
|
|
||
| The example demonstrates two approaches to error tracking: | ||
|
|
||
| Manual capture for specific critical operations** (`app/api/routes.py`). | ||
|
|
||
| ```python | ||
| 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 | ||
|
|
||
| ``` | ||
| 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 | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| """Flask application factory.""" | ||
|
|
||
| import posthog | ||
| 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 | ||
|
|
||
|
|
||
| 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) | ||
|
|
||
| # 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): | ||
| 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 | ||
|
|
||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| """API blueprint registration.""" | ||
|
|
||
| from flask import Blueprint | ||
|
|
||
| api_bp = Blueprint("api", __name__) | ||
|
|
||
| from app.api import routes # noqa: E402, F401 |
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,59 @@ | |||||||||||||||||||||||||||||||||||||||||
| """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("/test-error", methods=["POST"]) | |||||||||||||||||||||||||||||||||||||||||
| @login_required | |||||||||||||||||||||||||||||||||||||||||
| def test_error(): | |||||||||||||||||||||||||||||||||||||||||
| """Test endpoint demonstrating manual exception capture in PostHog. | |||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||
| Shows how to intentionally capture specific errors in PostHog. | |||||||||||||||||||||||||||||||||||||||||
| Use this pattern for critical operations where you want error tracking. | |||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||
| Query params: | |||||||||||||||||||||||||||||||||||||||||
| - capture: "true" to capture the exception in PostHog, "false" to just raise it | |||||||||||||||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||||||||||||||
| 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 | |||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||
| @@ -53,7 +53,10 @@ | ||
| "message": f"Error captured in PostHog. Reference ID: {event_id}" | ||
| }), 500 | ||
| else: | ||
| # Just return error without PostHog capture | ||
| return jsonify({"error": str(e)}), 500 | ||
| # Just return a generic error without exposing exception details | ||
| return jsonify({ | ||
| "error": "Operation failed", | ||
| "message": "An internal error occurred and was not captured in PostHog." | ||
| }), 500 | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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", "<ph_project_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, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = "main.home" | ||
| login_manager.login_message = "Please log in to access this page." |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what have I wrought