Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions basics/flask/.env.example
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
7 changes: 7 additions & 0 deletions basics/flask/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
.env
*.db
.venv/
venv/
instance/
129 changes: 129 additions & 0 deletions basics/flask/README.md
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
```
67 changes: 67 additions & 0 deletions basics/flask/app/__init__.py
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
7 changes: 7 additions & 0 deletions basics/flask/app/api/__init__.py
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
59 changes: 59 additions & 0 deletions basics/flask/app/api/routes.py
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"])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what have I wrought

@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

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI about 9 hours ago

In general, to fix information exposure via exceptions, the server should avoid returning raw exception messages or stack traces to the client. Instead, log the detailed error on the server side (or send it to an error tracking system) and return a generic, non-sensitive error message to the client, optionally with a reference ID.

In this code, the vulnerable behavior occurs in the else branch of the except block in test_error, which currently returns {"error": str(e)}. The best minimal fix, consistent with existing functionality, is to replace this with a generic error payload that does not include str(e). Since the True branch already defines a pattern ("Operation failed" plus an error ID and friendly message), we can mirror that style: keep the 500 status code and return a generic "Operation failed" and a brief description that no PostHog capture was requested, without including the exception text.

Concretely, in basics/flask/app/api/routes.py, in the test_error function’s else block (around line 56–57), replace jsonify({"error": str(e)}) with a jsonify call that uses a static message, for example:

return jsonify({
    "error": "Operation failed",
    "message": "An internal error occurred and was not captured in PostHog."
}), 500

No new imports or helpers are required.

Suggested changeset 1
basics/flask/app/api/routes.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/basics/flask/app/api/routes.py b/basics/flask/app/api/routes.py
--- a/basics/flask/app/api/routes.py
+++ b/basics/flask/app/api/routes.py
@@ -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
 
 
EOF
@@ -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


Copilot is powered by AI and may make mistakes. Always verify output.


40 changes: 40 additions & 0 deletions basics/flask/app/config.py
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,
}
10 changes: 10 additions & 0 deletions basics/flask/app/extensions.py
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."
7 changes: 7 additions & 0 deletions basics/flask/app/main/__init__.py
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
Loading