diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 31bcb90..8d307c8 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -39,9 +39,13 @@ jobs: - name: Determine version id: version + env: + INPUT_VERSION: ${{ github.event.inputs.version }} + EVENT_NAME: ${{ github.event_name }} + PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} run: | - if [ -n "${{ github.event.inputs.version }}" ]; then - VERSION="${{ github.event.inputs.version }}" + if [ -n "$INPUT_VERSION" ]; then + VERSION="$INPUT_VERSION" echo "Using manually specified version: ${VERSION}" else # Get the latest semver tag (ignore 'latest' tag), or use 0.0.0 if no tags exist @@ -54,19 +58,18 @@ jobs: echo "Latest semver tag found: ${LATEST_TAG}" # Remove 'v' prefix if present LATEST_VERSION=${LATEST_TAG#v} - + # Parse version components IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_VERSION" MAJOR=${VERSION_PARTS[0]:-0} MINOR=${VERSION_PARTS[1]:-0} PATCH=${VERSION_PARTS[2]:-0} - + # Determine bump type from PR labels BUMP_TYPE="patch" # Default to patch - if [ "${{ github.event_name }}" == "pull_request" ]; then - PR_LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}" + if [ "$EVENT_NAME" == "pull_request" ]; then echo "PR Labels: ${PR_LABELS}" - + if echo "${PR_LABELS}" | grep -q "major"; then BUMP_TYPE="major" elif echo "${PR_LABELS}" | grep -q "minor"; then @@ -75,9 +78,9 @@ jobs: BUMP_TYPE="patch" fi fi - + echo "Bump type: ${BUMP_TYPE}" - + # Increment version based on bump type case "${BUMP_TYPE}" in major) @@ -93,7 +96,7 @@ jobs: PATCH=$((PATCH + 1)) ;; esac - + VERSION="${MAJOR}.${MINOR}.${PATCH}" echo "Bumping from ${LATEST_VERSION} to ${VERSION} (${BUMP_TYPE})" fi diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 36e9fbd..8a2a544 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -68,22 +68,33 @@ jobs: - name: Send failure event to PostHog if: failure() + env: + COMMIT_SHA: ${{ github.sha }} + JOB_STATUS: ${{ job.status }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + COMMIT_AUTHOR: ${{ github.event.head_commit.author.name }} + GH_REF: ${{ github.ref }} + GH_WORKFLOW: ${{ github.workflow }} + RUN_ID: ${{ github.run_id }} + RUN_NUMBER: ${{ github.run_number }} + JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + MATRIX_EXAMPLE: ${{ matrix.example }} run: | curl -X POST https://webhooks.us.posthog.com/public/webhooks/019a7a81-7961-0000-d3e3-b5f34cc2a32b \ -H "Content-Type: application/json" \ - -d '{ - "event": "posthog-examples-repo-test-failure", - "commitSha": "${{ github.sha }}", - "jobStatus": "${{ job.status }}", - "commitMessage": "${{ github.event.head_commit.message }}", - "commitAuthor": "${{ github.event.head_commit.author.name }}", - "ref": "${{ github.ref }}", - "workflow": "${{ github.workflow }}", - "runId": "${{ github.run_id }}", - "runNumber": "${{ github.run_number }}", - "jobUrl": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "matrixExample": "${{ matrix.example }}" - }' + -d "$(jq -n \ + --arg event "posthog-examples-repo-test-failure" \ + --arg commitSha "$COMMIT_SHA" \ + --arg jobStatus "$JOB_STATUS" \ + --arg commitMessage "$COMMIT_MESSAGE" \ + --arg commitAuthor "$COMMIT_AUTHOR" \ + --arg ref "$GH_REF" \ + --arg workflow "$GH_WORKFLOW" \ + --arg runId "$RUN_ID" \ + --arg runNumber "$RUN_NUMBER" \ + --arg jobUrl "$JOB_URL" \ + --arg matrixExample "$MATRIX_EXAMPLE" \ + '{event: $event, commitSha: $commitSha, jobStatus: $jobStatus, commitMessage: $commitMessage, commitAuthor: $commitAuthor, ref: $ref, workflow: $workflow, runId: $runId, runNumber: $runNumber, jobUrl: $jobUrl, matrixExample: $matrixExample}')" - uses: actions/upload-artifact@v4 if: always() diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 6db280e..1b9fbf0 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -70,22 +70,33 @@ jobs: - name: Send failure event to PostHog if: failure() + env: + COMMIT_SHA: ${{ github.sha }} + JOB_STATUS: ${{ job.status }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + COMMIT_AUTHOR: ${{ github.event.head_commit.author.name }} + GH_REF: ${{ github.ref }} + GH_WORKFLOW: ${{ github.workflow }} + RUN_ID: ${{ github.run_id }} + RUN_NUMBER: ${{ github.run_number }} + JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + MATRIX_EXAMPLE: ${{ matrix.example }} run: | curl -X POST https://webhooks.us.posthog.com/public/webhooks/019a7a81-7961-0000-d3e3-b5f34cc2a32b \ -H "Content-Type: application/json" \ - -d '{ - "event": "posthog-examples-repo-test-failure", - "commitSha": "${{ github.sha }}", - "jobStatus": "${{ job.status }}", - "commitMessage": "${{ github.event.head_commit.message }}", - "commitAuthor": "${{ github.event.head_commit.author.name }}", - "ref": "${{ github.ref }}", - "workflow": "${{ github.workflow }}", - "runId": "${{ github.run_id }}", - "runNumber": "${{ github.run_number }}", - "jobUrl": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "example": "${{ matrix.example }}" - }' + -d "$(jq -n \ + --arg event "posthog-examples-repo-test-failure" \ + --arg commitSha "$COMMIT_SHA" \ + --arg jobStatus "$JOB_STATUS" \ + --arg commitMessage "$COMMIT_MESSAGE" \ + --arg commitAuthor "$COMMIT_AUTHOR" \ + --arg ref "$GH_REF" \ + --arg workflow "$GH_WORKFLOW" \ + --arg runId "$RUN_ID" \ + --arg runNumber "$RUN_NUMBER" \ + --arg jobUrl "$JOB_URL" \ + --arg example "$MATRIX_EXAMPLE" \ + '{event: $event, commitSha: $commitSha, jobStatus: $jobStatus, commitMessage: $commitMessage, commitAuthor: $commitAuthor, ref: $ref, workflow: $workflow, runId: $runId, runNumber: $runNumber, jobUrl: $jobUrl, example: $example}')" - uses: actions/upload-artifact@v4 if: always() diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..b6228b3 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,28 @@ +name: Security + +on: + pull_request: + push: + branches: + - main + +jobs: + semgrep-general: + name: Semgrep General + runs-on: ubuntu-24.04 + container: + image: returntocorp/semgrep + env: + SEMGREP_ENABLE_VERSION_CHECK: 'false' + steps: + - uses: actions/checkout@v4 + - run: | + semgrep \ + --config "p/owasp-top-ten" \ + --config "p/security-audit" \ + --config "p/trailofbits" \ + --config "p/github-actions" \ + --error \ + --metrics=off \ + --verbose \ + . diff --git a/.gitignore b/.gitignore index 102af2f..645aa71 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,14 @@ node_modules/ .pnp .pnp.* +# Python +venv/ +.venv/ +__pycache__/ +*.py[cod] +*.pyo +db.sqlite3 + # Environment variables .env .env.local diff --git a/basics/django/.env.example b/basics/django/.env.example new file mode 100644 index 0000000..366b061 --- /dev/null +++ b/basics/django/.env.example @@ -0,0 +1,4 @@ +POSTHOG_API_KEY= +POSTHOG_HOST=https://us.i.posthog.com +DJANGO_SECRET_KEY=your-secret-key-here +DEBUG=True diff --git a/basics/django/.gitignore b/basics/django/.gitignore new file mode 100644 index 0000000..7c4b178 --- /dev/null +++ b/basics/django/.gitignore @@ -0,0 +1,27 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Django stuff +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Environment +.env +.venv +env/ +venv/ + +# IDE +.vscode/ +.idea/ + +# Static files +staticfiles/ + +# Coverage +.coverage +htmlcov/ diff --git a/basics/django/README.md b/basics/django/README.md new file mode 100644 index 0000000..1dac31a --- /dev/null +++ b/basics/django/README.md @@ -0,0 +1,199 @@ +# PostHog Django example + +This is a [Django](https://djangoproject.com) example demonstrating PostHog integration with product analytics, error tracking, feature flags, and user identification. + +## Features + +- **Product analytics**: Track user events and behaviors +- **Error tracking**: Capture and track exceptions automatically +- **User identification**: Associate events with authenticated users via context +- **Feature flags**: Control feature rollouts with PostHog feature flags +- **Server-side tracking**: All tracking happens server-side with the Python SDK +- **Context middleware**: Automatic session and user context extraction + +## Getting started + +### 1. Install dependencies + +```bash +pip install posthog +``` + +### 2. Configure environment variables + +Create a `.env` file in the root directory: + +```bash +POSTHOG_API_KEY=your_posthog_project_api_key +POSTHOG_HOST=https://us.i.posthog.com +``` + +Get your PostHog API key from your [PostHog project settings](https://app.posthog.com/project/settings). + +### 3. Run migrations + +```bash +python manage.py migrate +``` + +### 4. Run the development server + +```bash +python manage.py runserver +``` + +Open [http://localhost:8000](http://localhost:8000) with your browser to see the app. + +## Project structure + +``` +django/ +├── manage.py # Django management script +├── requirements.txt # Python dependencies +├── .env.example # Environment variable template +├── .gitignore +├── posthog_example/ +│ ├── __init__.py +│ ├── settings.py # Django settings with PostHog config +│ ├── urls.py # URL routing +│ ├── wsgi.py # WSGI application +│ └── asgi.py # ASGI application +└── core/ + ├── __init__.py + ├── apps.py # AppConfig with PostHog initialization + ├── views.py # Views with event tracking examples + ├── urls.py # App URL patterns + └── templates/ + └── core/ + ├── base.html # Base template + ├── home.html # Home/login page + ├── burrito.html # Burrito page with event tracking + ├── dashboard.html # Dashboard with feature flag example + └── profile.html # Profile page +``` + +## Key integration points + +### PostHog initialization (core/apps.py) + +```python +import posthog +from django.conf import settings + +class CoreConfig(AppConfig): + name = 'core' + + def ready(self): + posthog.api_key = settings.POSTHOG_API_KEY + posthog.host = settings.POSTHOG_HOST +``` + +### Django settings configuration (settings.py) + +```python +import os + +# PostHog configuration +POSTHOG_API_KEY = os.environ.get('POSTHOG_API_KEY', '') +POSTHOG_HOST = os.environ.get('POSTHOG_HOST', 'https://us.i.posthog.com') + +MIDDLEWARE = [ + # ... other middleware + 'posthog.integrations.django.PosthogContextMiddleware', +] +``` + +### Built-in context middleware + +The PostHog SDK includes a Django middleware that automatically wraps all requests with a context. It extracts session and user information from request headers and tags all events captured during the request. + +The middleware automatically extracts: + +- **Session ID** from the `X-POSTHOG-SESSION-ID` header +- **Distinct ID** from the `X-POSTHOG-DISTINCT-ID` header +- **Current URL** as `$current_url` +- **Request method** as `$request_method` + +### User identification (core/views.py) + +```python +import posthog + +def login_view(request): + # ... authentication logic + if user: + with posthog.new_context(): + posthog.identify_context(str(user.id)) + posthog.tag('email', user.email) + posthog.tag('username', user.username) + posthog.capture('user_logged_in', properties={ + 'login_method': 'email', + }) +``` + +### Event tracking (core/views.py) + +```python +import posthog + +def consider_burrito(request): + user_id = str(request.user.id) if request.user.is_authenticated else 'anonymous' + + with posthog.new_context(): + posthog.identify_context(user_id) + posthog.capture('burrito_considered', properties={ + 'total_considerations': request.session.get('burrito_count', 0), + }) +``` + +### Feature flags (core/views.py) + +```python +import posthog + +def dashboard_view(request): + user_id = str(request.user.id) if request.user.is_authenticated else 'anonymous' + + show_new_feature = posthog.feature_enabled( + 'new-dashboard-feature', + distinct_id=user_id + ) + + return render(request, 'core/dashboard.html', { + 'show_new_feature': show_new_feature + }) +``` + +### Error tracking (core/views.py) + +Capture exceptions manually using `capture_exception()`: + +```python +import posthog + +def profile_view(request): + try: + risky_operation() + except Exception as e: + posthog.capture_exception(e) +``` + +## Frontend integration (optional) + +If you're using PostHog's JavaScript SDK on the frontend, enable tracing headers to connect frontend sessions with backend events: + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + __add_tracing_headers: ['your-backend-domain.com'], +}) +``` + +This automatically adds `X-POSTHOG-SESSION-ID` and `X-POSTHOG-DISTINCT-ID` headers to requests, which the Django middleware extracts to maintain context. + +## Learn more + +- [PostHog Django integration](https://posthog.com/docs/libraries/django) +- [PostHog Python SDK](https://posthog.com/docs/libraries/python) +- [PostHog documentation](https://posthog.com/docs) +- [Django documentation](https://docs.djangoproject.com/) diff --git a/basics/django/core/__init__.py b/basics/django/core/__init__.py new file mode 100644 index 0000000..e471232 --- /dev/null +++ b/basics/django/core/__init__.py @@ -0,0 +1 @@ +# Core app for PostHog Django example diff --git a/basics/django/core/apps.py b/basics/django/core/apps.py new file mode 100644 index 0000000..db0c6aa --- /dev/null +++ b/basics/django/core/apps.py @@ -0,0 +1,37 @@ +""" +Django AppConfig that initializes PostHog when the application starts. + +This ensures the SDK is configured once when Django starts, making it available throughout the application. +""" + +from django.apps import AppConfig +from django.conf import settings + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core' + + def ready(self): + """ + Initialize PostHog when Django starts. + + This method is called once when Django starts. We configure the + PostHog SDK here so it's available everywhere in the application. + + Note: Import posthog inside this method to avoid import issues + during Django's startup sequence. + """ + import posthog + + # Configure PostHog with settings from Django settings + posthog.api_key = settings.POSTHOG_API_KEY + posthog.host = settings.POSTHOG_HOST + + # Disable PostHog if configured (useful for testing) + if settings.POSTHOG_DISABLED: + posthog.disabled = True + + # Optional: Enable debug mode in development + if settings.DEBUG: + posthog.debug = True diff --git a/basics/django/core/templates/core/base.html b/basics/django/core/templates/core/base.html new file mode 100644 index 0000000..2f8b667 --- /dev/null +++ b/basics/django/core/templates/core/base.html @@ -0,0 +1,138 @@ + + + + + + {% block title %}PostHog Django example{% endblock %} + + + + {% if user.is_authenticated %} + + {% endif %} + +
+ {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + + {% block content %}{% endblock %} +
+ + {% block scripts %}{% endblock %} + + diff --git a/basics/django/core/templates/core/burrito.html b/basics/django/core/templates/core/burrito.html new file mode 100644 index 0000000..ebcc79f --- /dev/null +++ b/basics/django/core/templates/core/burrito.html @@ -0,0 +1,54 @@ +{% extends 'core/base.html' %} + +{% block title %}Burrito - PostHog Django example{% endblock %} + +{% block content %} +
+

Burrito consideration tracker

+

This page demonstrates custom event tracking with PostHog.

+
+ +
+

Times considered

+
{{ burrito_count }}
+ +
+ +
+

How event tracking works

+

Each time you click the button, a burrito_considered event is sent to PostHog:

+
from posthog import new_context, identify_context, capture
+
+with new_context():
+    identify_context(user_id)
+    capture('burrito_considered', properties={
+        'total_considerations': count,
+    })
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/basics/django/core/templates/core/dashboard.html b/basics/django/core/templates/core/dashboard.html new file mode 100644 index 0000000..ec59ed1 --- /dev/null +++ b/basics/django/core/templates/core/dashboard.html @@ -0,0 +1,54 @@ +{% extends 'core/base.html' %} + +{% block title %}Dashboard - PostHog Django example{% endblock %} + +{% block content %} +
+

Dashboard

+

Welcome back, {{ user.username }}!

+
+ +
+

Feature flags

+

Feature flags allow you to control feature rollouts and run A/B tests.

+ + {% if show_new_feature %} +
+

New feature enabled!

+

+ This section is only visible because the new-dashboard-feature + flag is enabled for your user. +

+ {% if feature_config %} +

Feature config: {{ feature_config }}

+ {% endif %} +
+ {% else %} +
+

+ The new-dashboard-feature flag is not enabled for your user. + Create this flag in your PostHog project to see it in action. +

+
+ {% endif %} +
+ +
+

How feature flags work

+
# Check if a feature flag is enabled
+show_feature = posthog.feature_enabled(
+    'new-dashboard-feature',
+    distinct_id=user_id,
+    person_properties={
+        'email': user.email,
+        'is_staff': user.is_staff,
+    }
+)
+
+# Get feature flag payload for configuration
+config = posthog.get_feature_flag_payload(
+    'new-dashboard-feature',
+    distinct_id=user_id,
+)
+
+{% endblock %} diff --git a/basics/django/core/templates/core/home.html b/basics/django/core/templates/core/home.html new file mode 100644 index 0000000..cbd3d39 --- /dev/null +++ b/basics/django/core/templates/core/home.html @@ -0,0 +1,37 @@ +{% extends 'core/base.html' %} + +{% block title %}Login - PostHog Django example{% endblock %} + +{% block content %} +
+

PostHog Django example

+

Welcome! This example demonstrates PostHog integration with Django.

+
+ +
+

Login

+

Login to see PostHog analytics in action.

+ +
+ {% csrf_token %} + + + +
+ +

+ Tip: Create a user with python manage.py createsuperuser +

+
+ +
+

What this example demonstrates

+
    +
  • User identification - Users are identified with identify_context() on login
  • +
  • Pageview tracking - Middleware extracts session and user context
  • +
  • Event tracking - Custom events captured with capture() in context
  • +
  • Feature flags - Conditional features with posthog.feature_enabled()
  • +
  • Error tracking - Exceptions captured with capture_exception()
  • +
+
+{% endblock %} diff --git a/basics/django/core/templates/core/profile.html b/basics/django/core/templates/core/profile.html new file mode 100644 index 0000000..1531e47 --- /dev/null +++ b/basics/django/core/templates/core/profile.html @@ -0,0 +1,97 @@ +{% extends 'core/base.html' %} + +{% block title %}Profile - PostHog Django example{% endblock %} + +{% block content %} +
+

Profile

+

This page demonstrates error tracking with PostHog.

+
+ +
+

User information

+ + + + + + + + + + + + + + + + + +
Username:{{ user.username }}
Email:{{ user.email|default:"Not set" }}
Date Joined:{{ user.date_joined }}
Staff Status:{{ user.is_staff|yesno:"Yes,No" }}
+
+ +
+

Error tracking demo

+

Click the buttons below to trigger different types of errors. These errors are caught and sent to PostHog.

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

How error tracking works

+
import posthog
+
+try:
+    risky_operation()
+except Exception as e:
+    posthog.capture_exception(e)
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/basics/django/core/urls.py b/basics/django/core/urls.py new file mode 100644 index 0000000..9813f72 --- /dev/null +++ b/basics/django/core/urls.py @@ -0,0 +1,30 @@ +""" +URL configuration for the core app. + +This module defines all the URL patterns for the PostHog example views. +""" + +from django.urls import path +from . import views + +urlpatterns = [ + # Home login page + path('', views.home_view, name='home'), + + # Authentication + path('logout/', views.logout_view, name='logout'), + + # Dashboard with feature flags + path('dashboard/', views.dashboard_view, name='dashboard'), + + # Burrito example for event tracking + path('burrito/', views.burrito_view, name='burrito'), + path('api/burrito/consider/', views.consider_burrito_view, name='consider_burrito'), + + # Profile with error tracking + path('profile/', views.profile_view, name='profile'), + path('api/trigger-error/', views.trigger_error_view, name='trigger_error'), + + # Group analytics example + path('api/group-analytics/', views.group_analytics_view, name='group_analytics'), +] diff --git a/basics/django/core/views.py b/basics/django/core/views.py new file mode 100644 index 0000000..6bf534d --- /dev/null +++ b/basics/django/core/views.py @@ -0,0 +1,219 @@ +"""Django views demonstrating PostHog integration patterns""" + +import posthog +from posthog import new_context, identify_context, tag, capture +from django.shortcuts import render, redirect +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.http import JsonResponse +from django.views.decorators.http import require_POST + + +def home_view(request): + """Home page with login functionality""" + if request.user.is_authenticated: + return redirect('dashboard') + + if request.method == 'POST': + username = request.POST.get('username') + password = request.POST.get('password') + + user = authenticate(request, username=username, password=password) + + if user is not None: + login(request, user) + + # PostHog: Identify user and capture login event + with new_context(): + identify_context(str(user.id)) + + # Set person properties (PII goes in tag, not capture) + tag('email', user.email) + tag('username', user.username) + tag('name', user.get_full_name() or user.username) + tag('is_staff', user.is_staff) + tag('date_joined', user.date_joined.isoformat()) + + capture('user_logged_in', properties={ + 'login_method': 'email', + }) + + return redirect('dashboard') + else: + messages.error(request, 'Invalid username or password') + + return render(request, 'core/home.html') + + +def logout_view(request): + """Logout the current user""" + if request.user.is_authenticated: + user_id = str(request.user.id) + + # PostHog: Track logout before session ends + with new_context(): + identify_context(user_id) + capture('user_logged_out') + + logout(request) + + return redirect('home') + + +@login_required +def dashboard_view(request): + """Dashboard page with feature flag example""" + user_id = str(request.user.id) + + # PostHog: Track dashboard view + with new_context(): + identify_context(user_id) + capture('dashboard_viewed', properties={ + 'is_staff': request.user.is_staff, + }) + + # PostHog: Check feature flag + show_new_feature = posthog.feature_enabled( + 'new-dashboard-feature', + distinct_id=user_id, + person_properties={ + 'email': request.user.email, + 'is_staff': request.user.is_staff, + } + ) + + # PostHog: Get feature flag payload + feature_config = posthog.get_feature_flag_payload( + 'new-dashboard-feature', + distinct_id=user_id, + ) + + context = { + 'show_new_feature': show_new_feature, + 'feature_config': feature_config, + } + + return render(request, 'core/dashboard.html', context) + + +@login_required +def burrito_view(request): + """Example page demonstrating event tracking""" + count = request.session.get('burrito_count', 0) + + context = { + 'burrito_count': count, + } + + return render(request, 'core/burrito.html', context) + + +@login_required +@require_POST +def consider_burrito_view(request): + """API endpoint for tracking burrito considerations""" + count = request.session.get('burrito_count', 0) + 1 + request.session['burrito_count'] = count + + user_id = str(request.user.id) + + # PostHog: Track custom event + with new_context(): + identify_context(user_id) + capture('burrito_considered', properties={ + 'total_considerations': count, + }) + + return JsonResponse({ + 'success': True, + 'count': count, + }) + + +@login_required +def profile_view(request): + """Profile page with error tracking demonstration""" + user_id = str(request.user.id) + + # PostHog: Track profile view + with new_context(): + identify_context(user_id) + capture('profile_viewed') + + context = { + 'user': request.user, + } + + return render(request, 'core/profile.html', context) + + +@login_required +@require_POST +def trigger_error_view(request): + """API endpoint that demonstrates error tracking""" + try: + error_type = request.POST.get('error_type', 'generic') + + if error_type == 'value': + raise ValueError("Invalid value provided by user") + elif error_type == 'key': + data = {} + _ = data['nonexistent_key'] + else: + raise Exception("Something went wrong!") + + except Exception as e: + # PostHog: Capture exception + posthog.capture_exception(e) + + # PostHog: Track error trigger event + with new_context(): + identify_context(str(request.user.id)) + capture('error_triggered', properties={ + 'error_type': error_type, + 'error_message': str(e), + }) + + return JsonResponse({ + 'success': False, + 'error': str(e), + 'message': 'Error has been captured by PostHog', + }, status=400) + + return JsonResponse({'success': True}) + + +@login_required +def group_analytics_view(request): + """Example demonstrating group analytics""" + user_id = str(request.user.id) + + # PostHog: Identify group + posthog.group_identify( + group_type='company', + group_key='acme-corp', + properties={ + 'name': 'Acme Corporation', + 'plan': 'enterprise', + 'employee_count': 150, + } + ) + + # PostHog: Capture event with group + with new_context(): + identify_context(user_id) + capture( + 'feature_used', + properties={ + 'feature_name': 'group_analytics', + }, + groups={ + 'company': 'acme-corp', + } + ) + + return JsonResponse({ + 'success': True, + 'message': 'Group analytics event captured', + }) diff --git a/basics/django/manage.py b/basics/django/manage.py new file mode 100644 index 0000000..7fc4b11 --- /dev/null +++ b/basics/django/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'posthog_example.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/basics/django/posthog_example/__init__.py b/basics/django/posthog_example/__init__.py new file mode 100644 index 0000000..7d233d7 --- /dev/null +++ b/basics/django/posthog_example/__init__.py @@ -0,0 +1 @@ +# PostHog Django example project diff --git a/basics/django/posthog_example/asgi.py b/basics/django/posthog_example/asgi.py new file mode 100644 index 0000000..5232a47 --- /dev/null +++ b/basics/django/posthog_example/asgi.py @@ -0,0 +1,11 @@ +""" +ASGI config for PostHog example project +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'posthog_example.settings') + +application = get_asgi_application() diff --git a/basics/django/posthog_example/settings.py b/basics/django/posthog_example/settings.py new file mode 100644 index 0000000..167c4b3 --- /dev/null +++ b/basics/django/posthog_example/settings.py @@ -0,0 +1,89 @@ +"""Django settings for PostHog example project""" + +import os +from pathlib import Path + +try: + from dotenv import load_dotenv + load_dotenv() +except ImportError: + pass + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-example-key-change-in-production') + +DEBUG = os.environ.get('DEBUG', 'True').lower() == 'true' + +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] + + +# 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' + + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'core.apps.CoreConfig', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'posthog.integrations.django.PosthogContextMiddleware', +] + +ROOT_URLCONF = 'posthog_example.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'posthog_example.wsgi.application' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +STATIC_URL = 'static/' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/basics/django/posthog_example/urls.py b/basics/django/posthog_example/urls.py new file mode 100644 index 0000000..f9bc6b4 --- /dev/null +++ b/basics/django/posthog_example/urls.py @@ -0,0 +1,12 @@ +""" +URL configuration for PostHog example project +""" + +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + # Include the core app URLs for PostHog examples + path('', include('core.urls')), +] diff --git a/basics/django/posthog_example/wsgi.py b/basics/django/posthog_example/wsgi.py new file mode 100644 index 0000000..b20deb9 --- /dev/null +++ b/basics/django/posthog_example/wsgi.py @@ -0,0 +1,11 @@ +""" +WSGI config for PostHog example project +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'posthog_example.settings') + +application = get_wsgi_application() diff --git a/basics/django/requirements.txt b/basics/django/requirements.txt new file mode 100644 index 0000000..d89fcae --- /dev/null +++ b/basics/django/requirements.txt @@ -0,0 +1,3 @@ +Django>=4.2,<5.0 +posthog # Always use latest version +python-dotenv>=1.0.0 diff --git a/llm-prompts/basic-integration/1.0-begin.md b/llm-prompts/basic-integration/1.0-begin.md index 1578b18..70a7f98 100644 --- a/llm-prompts/basic-integration/1.0-begin.md +++ b/llm-prompts/basic-integration/1.0-begin.md @@ -7,17 +7,7 @@ We're making an event tracking plan for this project. 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. - -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. +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 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. 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: event name, event description, and the file path we want to place the event in. If events already exist, don't duplicate them; supplement them. diff --git a/llm-prompts/basic-integration/1.1-edit.md b/llm-prompts/basic-integration/1.1-edit.md index 7df2c7d..03d6738 100644 --- a/llm-prompts/basic-integration/1.1-edit.md +++ b/llm-prompts/basic-integration/1.1-edit.md @@ -17,7 +17,7 @@ Where possible, add calls for PostHog's identify() function on the client side u 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. +You should also add PostHog error tracking to these files where relevant. Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. @@ -30,3 +30,8 @@ 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 + +## Notes on react-based projects: PAY CLOSE ATTENTION + +- NEVER USE useEffect(); this is brittle and causes errors. Instead, add appropriate event handlers in places where you are tempted to use useEffect(). This is appropriate to our analytics capture approach anyway. +- Prefer event handlers or routing mechanisms to trigger analytics calls diff --git a/llm-prompts/basic-integration/1.2-revise.md b/llm-prompts/basic-integration/1.2-revise.md index dbc1d0d..2b0ba9d 100644 --- a/llm-prompts/basic-integration/1.2-revise.md +++ b/llm-prompts/basic-integration/1.2-revise.md @@ -5,8 +5,6 @@ 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. -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. ## Status diff --git a/llm-prompts/basic-integration/1.3-conclude.md b/llm-prompts/basic-integration/1.3-conclude.md index 552118f..978c321 100644 --- a/llm-prompts/basic-integration/1.3-conclude.md +++ b/llm-prompts/basic-integration/1.3-conclude.md @@ -20,10 +20,6 @@ We've built some insights and a dashboard for you to keep an eye on user behavio [links] -### 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. - Upon completion, remove .posthog-events.json. @@ -32,5 +28,5 @@ Upon completion, remove .posthog-events.json. 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 +- Configuring insights and dashboard (with URLs) +- Creating setup report (with file path) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2833cd6..a110bd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@posthog/examples", - "version": "1.0.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@posthog/examples", - "version": "1.0.0", + "version": "1.2.0", "dependencies": { "gray-matter": "^4.0.3", "js-yaml": "^4.1.1" @@ -2221,6 +2221,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index 5880cf8..8cc8b06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@posthog/examples", - "version": "1.1.0", + "version": "1.2.0", "private": true, "description": "PostHog example projects", "scripts": { diff --git a/scripts/build-examples-mcp-resources.js b/scripts/build-examples-mcp-resources.js index 5c6298a..5f9fef8 100644 --- a/scripts/build-examples-mcp-resources.js +++ b/scripts/build-examples-mcp-resources.js @@ -101,6 +101,12 @@ const DOCS_CONFIG = { name: 'PostHog React Router v7 Declarative mode integration documentation', description: 'PostHog integration documentation for React Router v7 Declarative mode', url: 'https://posthog.com/docs/libraries/react-router/react-router-v7-declarative-mode' + }, + 'django': { + id: 'django', + name: 'PostHog Django integration documentation', + description: 'PostHog integration documentation for Django', + url: 'https://posthog.com/docs/libraries/django' } } }; @@ -180,6 +186,24 @@ const defaultConfig = { regex: [], }, plugins: [], + }, + { + path: 'basics/django', + id: 'django', + displayName: 'Django', + tags: ['django', 'python', 'server-side'], + skipPatterns: { + includes: [ + '__pycache__', + '.pyc', + 'db.sqlite3', + '.venv', + 'venv', + 'env', + ], + regex: [], + }, + plugins: [], } ], globalSkipPatterns: { diff --git a/transformation-config/commandments.yaml b/transformation-config/commandments.yaml index 2ad7fc0..050059e 100644 --- a/transformation-config/commandments.yaml +++ b/transformation-config/commandments.yaml @@ -11,4 +11,12 @@ commandments: javascript: - Remember that source code is available in the node_modules directory + python: + - Remember that source code is available in the venv/site-packages directory + + django: + - Ensure PostHog middleware is placed after AuthenticationMiddleware in MIDDLEWARE settings + - Use Django's request object to access user and session data for PostHog identify calls + + \ No newline at end of file diff --git a/transformation-config/skills.yaml b/transformation-config/skills.yaml index f7df8c8..1f6efe6 100644 --- a/transformation-config/skills.yaml +++ b/transformation-config/skills.yaml @@ -61,6 +61,15 @@ skills: docs_urls: - https://posthog.com/docs/libraries/react-router/react-router-v7-declarative-mode.md + - id: django + type: example + example_path: basics/django + display_name: Django + description: PostHog integration for Django applications + tags: [django, python] + docs_urls: + - https://posthog.com/docs/libraries/django.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 f010faf..08c09c5 100644 --- a/transformation-config/skip-patterns.yaml +++ b/transformation-config/skip-patterns.yaml @@ -58,6 +58,10 @@ global: - eslint - repomix-output.xml + # Python + - venv + - .pyc + # Regex patterns - skip if path matches # Note: Patterns are JavaScript regex syntax regex: