diff --git a/apps/basic-integration/flask/flask3-social-media/.claude/skills/integration-flask/.posthog-wizard b/apps/basic-integration/flask/flask3-social-media/.claude/skills/integration-flask/.posthog-wizard new file mode 100644 index 000000000..e69de29bb diff --git a/apps/basic-integration/flask/flask3-social-media/.claude/skills/integration-flask/references/flask.md b/apps/basic-integration/flask/flask3-social-media/.claude/skills/integration-flask/references/flask.md new file mode 100644 index 000000000..560fa82f0 --- /dev/null +++ b/apps/basic-integration/flask/flask3-social-media/.claude/skills/integration-flask/references/flask.md @@ -0,0 +1,147 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Flask - Docs + +Copy page + +# Flask - Docs + +PostHog makes it easy to get data about traffic and usage of your Flask app. Integrating PostHog enables analytics, custom events capture, feature flags, error tracking, and more. + +This guide walks you through integrating PostHog into your Flask app using the [Python SDK](/docs/libraries/python.md). + +> These docs cover version `7.x` of the Python SDK, which requires Python 3.10 or higher. On Python 3.9? See [supported versions](#supported-versions). + +## Installation + +To start, run `pip install posthog` to install PostHog’s Python SDK. + +Then, initialize PostHog where you'd like to use it. For example, here's how to capture an event in a simple route: + +app.py + +PostHog AI + +```python +from flask import Flask +from posthog import Posthog +app = Flask(__name__) +posthog = Posthog( + '', + host='https://us.i.posthog.com', +) +@app.route('/api/dashboard', methods=['POST']) +def api_dashboard(): + posthog.capture( + 'dashboard_api_called', + distinct_id='distinct_id_of_your_user', + ) + return '', 204 +``` + +You can find your project token and instance address in [your project settings](https://app.posthog.com/project/settings). + +## Identifying users + +> **Identifying users is required.** Backend events need a `distinct_id` to associate events with the correct user. +> +> In Python, you can do this through a context. All event captures in the same context will be tagged automatically with the correct `distinct_id`. Typically, you would set a fresh context and identify at the top of each route. +> +> Python +> +> PostHog AI +> +> ```python +> from posthog import new_context, identify_context, capture +> @app.get("/foo") +> def foo(current_user: User = Depends(get_current_user)): +> with new_context(): # Set context at the top of a route +> identify_context(current_user.id) +> capture("foo_viewed") +> return {"status": "ok"} +> ``` +> +> When possible, write a small piece of **middleware** that resolves your authenticated user, wrap a context around the request, and identifies it. Every `capture()` downstream is then attributed *automatically*. The SDK's Django middleware does this automatically and you can replicate it when using the plain Python SDK. + +## Request contexts + +Use [contexts](/docs/libraries/python.md#contexts) to share identity, session IDs, and tags across multiple captures during a request. + +If you're using [PostHog JavaScript Web](/docs/libraries/js.md) on the frontend, configure [`tracing_headers`](/docs/libraries/js/config.md#tracing-headers) for your Flask backend hostname so browser requests include the session and distinct ID headers. + +Then read the incoming headers in your Flask request handler. Tracing headers are client-controlled analytics context, not authentication or authorization, so prefer your authenticated user ID when one is available: + +Python + +PostHog AI + +```python +from flask import request, session +from posthog import identify_context, set_context_session, tag +@app.route('/api/dashboard', methods=['POST']) +def api_dashboard(): + with posthog.new_context(fresh=True): + distinct_id = session.get('user_id') or request.headers.get('X-POSTHOG-DISTINCT-ID') + if distinct_id: + identify_context(str(distinct_id)) + session_id = request.headers.get('X-POSTHOG-SESSION-ID') + if session_id: + set_context_session(session_id) + tag('$current_url', request.url) + tag('$request_method', request.method) + tag('$request_path', request.path) + posthog.capture('dashboard_api_called') + return '', 204 +``` + +Events captured without a context or explicit `distinct_id` are sent as [anonymous events](/docs/data/anonymous-vs-identified-events.md) with an auto-generated `distinct_id`. See the [Python SDK docs](/docs/libraries/python.md#person-profiles-and-properties) for more details. + +## Error tracking + +Flask has built-in error handlers. This means PostHog’s default exception autocapture won’t work and we need to manually capture errors instead using `capture_exception()`: + +Python + +PostHog AI + +```python +from flask import Flask, jsonify +from posthog import Posthog +app = Flask(__name__) +posthog = Posthog('', host='https://us.i.posthog.com') +@app.errorhandler(Exception) +def handle_exception(e): + # Capture methods, including capture_exception, return the UUID of the captured event, + # which you can use to find specific errors users encountered + event_id = posthog.capture_exception(e) + # You can show the event ID to your user, and ask them to include it in bug reports + response = jsonify({'message': str(e), 'error_id': event_id}) + response.status_code = 500 + return response +``` + +## Next steps + +For any technical questions for how to integrate specific PostHog features into Flask (such as analytics, feature flags, A/B testing, etc.), have a look at our [Python SDK docs](/docs/libraries/python.md). + +Alternatively, the following tutorials can help you get started: + +- [How to set up analytics in Python and Flask](/tutorials/python-analytics.md) +- [How to set up feature flags in Python and Flask](/tutorials/python-feature-flags.md) +- [How to set up A/B tests in Python and Flask](/tutorials/python-ab-testing.md) + +## Supported versions + +These docs cover version `7.x` of the PostHog Python SDK, which requires Python 3.10 or higher. Python 3.9 is no longer supported on `7.x.x` and higher — pin to the 6.x line with `pip install 'posthog<7'`, where `6.9.3` is the final release. + +Everything on this page works the same way on `6.9.3`. Event capture, the context API (`new_context`, `identify_context`, `set_context_session`), and `PosthogContextMiddleware` are identical on `6.9.3` and `7.0.0` — `7.0.0` only dropped Python 3.9 and bumped the optional LLM provider SDKs. That includes the middleware identifying the request context from the `X-POSTHOG-DISTINCT-ID` header and falling back to the authenticated user, which behaves the same across both lines. + +Later `7.x` releases add what the 6.x line does not receive, such as the Celery integration, tracing header sanitization, and `set_context_device_id`. They also changed the middleware's own captured properties: `7.x` sends the request IP as `$ip`, where `6.9.3` sends it as `$ip_address`, and `7.x` additionally captures `$request_path`, `$raw_user_agent`, and the authenticated user's `email`. + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/flask/flask3-social-media/.claude/skills/integration-flask/references/identify-users.md b/apps/basic-integration/flask/flask3-social-media/.claude/skills/integration-flask/references/identify-users.md new file mode 100644 index 000000000..8647dcb37 --- /dev/null +++ b/apps/basic-integration/flask/flask3-social-media/.claude/skills/integration-flask/references/identify-users.md @@ -0,0 +1,307 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Identify users - Docs + +Copy page + +# Identify users - Docs + +Linking events to specific users enables you to build a full picture of how they're using your product across different sessions, devices, and platforms. + +This is straightforward to do when [capturing backend events](/docs/product-analytics/capture-events?tab=Node.js.md), as you associate events to a specific user using a `distinct_id`, which is a required argument. + +However, in the frontend of a [web](/docs/libraries/js/usage.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/usage.md#capturing-anonymous-events). + +To link events to specific users, call `identify`: + +PostHog AI + +### Web + +```javascript +posthog.identify( + 'distinct_id', // Replace 'distinct_id' with your user's unique identifier + { email: 'max@hedgehogmail.com', name: 'Max Hedgehog' } // optional: set additional person properties +); +``` + +### Android + +```kotlin +PostHog.identify( + distinctId = distinctID, // Replace 'distinctID' with your user's unique identifier + // optional: set additional person properties + userProperties = mapOf( + "name" to "Max Hedgehog", + "email" to "max@hedgehogmail.com" + ) +) +``` + +### iOS + +```swift +PostHogSDK.shared.identify("distinct_id", // Replace "distinct_id" with your user's unique identifier + userProperties: ["name": "Max Hedgehog", "email": "max@hedgehogmail.com"]) // optional: set additional person properties +``` + +### React Native + +```jsx +posthog.identify('distinct_id', { // Replace "distinct_id" with your user's unique identifier + email: 'max@hedgehogmail.com', // optional: set additional person properties + name: 'Max Hedgehog' +}) +``` + +### Dart + +```dart +await Posthog().identify( + userId: 'distinct_id', // Replace "distinct_id" with your user's unique identifier + userProperties: { + 'email': 'max@hedgehogmail.com', // optional: set additional person properties + 'name': 'Max Hedgehog', + }, +); +``` + +Events captured after calling `identify` are identified events and this creates a person profile if one doesn't exist already. + +Due to the cost of processing them, anonymous events can be up to 4x cheaper than identified events, so it's recommended you only capture identified events when needed. + +## How identify works + +When a user starts browsing your website or app, PostHog automatically assigns them an **anonymous ID**, which is stored locally. + +Provided you've [configured persistence](/docs/libraries/js/persistence.md) to use cookies or `localStorage`, this enables us to track anonymous users – even across different sessions. + +By calling `identify` with a `distinct_id` of your choice (usually the user's ID in your database, or their email), you link the anonymous ID and distinct ID together. + +Thus, all past and future events made with that anonymous ID are now associated with the distinct ID. + +This enables you to do things like associate events with a user from before they log in for the first time, or associate their events across different devices or platforms. + +Using identify in the backend + +Although you can call `identify` using our backend SDKs, it is used most in frontends. This is because there is no concept of anonymous sessions in the backend SDKs, so calling `identify` only updates person profiles. + +## Best practices when using `identify` + +### 1\. Call `identify` as soon as you're able to + +In your frontend, you should call `identify` as soon as you're able to. + +Typically, this is every time your **app loads** for the first time, and directly after your **users log in**. + +This ensures that events sent during your users' sessions are correctly associated with them. + +You only need to call `identify` once per session, and you should avoid calling it multiple times unnecessarily. + +If you call `identify` multiple times with the same data without reloading the page in between, PostHog will ignore the subsequent calls. + +#### Identify users when the web SDK loads + +If your app already knows the signed-in user when you initialize the JavaScript web SDK, the [`loaded` callback](/docs/libraries/js/config.md) is a convenient place to call `identify`. This identifies the user as soon as the SDK has loaded: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + loaded: (posthog) => { + if (currentUser?.id) { + posthog.identify(currentUser.id, { + email: currentUser.email, + name: currentUser.name, + }) + } + }, +}) +``` + +In this example, `currentUser` represents user data already available from your authentication system. If your app loads the user asynchronously, call `posthog.identify()` as soon as that data becomes available instead. + +### 2\. Use unique strings for distinct IDs + +If two users have the same distinct ID, their data is merged and they are considered one user in PostHog. Two common ways this can happen are: + +- Your logic for generating IDs does not generate sufficiently strong IDs and you can end up with a clash where 2 users have the same ID. +- There's a bug, typo, or mistake in your code leading to most or all users being identified with generic IDs like `null`, `true`, or `distinctId`. + +PostHog also has built-in protections to stop the most common distinct ID mistakes. + +### 3\. Reset after logout + +If a user logs out on your frontend, you should call `reset()` to unlink any future events made on that device with that user. + +This is important if your users are sharing a computer, as otherwise all of those users are grouped together into a single user due to shared cookies between sessions. + +**We strongly recommend you call `reset` on logout even if you don't expect users to share a computer.** + +You can do that like so: + +PostHog AI + +### Web + +```javascript +posthog.reset() +``` + +### iOS + +```swift +PostHogSDK.shared.reset() +``` + +### Android + +```kotlin +PostHog.reset() +``` + +### React Native + +```jsx +posthog.reset() +``` + +### Dart + +```dart +await Posthog().reset(); +``` + +If you *also* want to reset the `device_id` so that the device will be considered a new device in future events, you can pass `true` as an argument: + +Web + +PostHog AI + +```javascript +posthog.reset(true) +``` + +### 4\. Person profiles and properties + +You'll notice that one of the parameters in the `identify` method is a `properties` object. + +This enables you to set [person properties](/docs/product-analytics/person-properties.md). + +Whenever possible, we recommend passing in all person properties you have available each time you call identify, as this ensures their person profile on PostHog is up to date. + +Person properties can also be set being adding a `$set` property to a event `capture` call. + +**\`$set\` and \`$set\_once\` aren't stored on events** + +These properties only tell PostHog how to update person data during ingestion — they aren't kept on the stored event, so you can't filter, break down, or query events by them. To query the values you set, use [person properties](/docs/product-analytics/person-properties.md) instead. + +See our [person properties docs](/docs/product-analytics/person-properties.md) for more details on how to work with them and best practices. + +### 5\. Use deep links between platforms + +We recommend you call `identify` [as soon as you're able](#1-call-identify-as-soon-as-youre-able), typically when a user signs up or logs in. + +This doesn't work if one or both platforms are unauthenticated. Some examples of such cases are: + +- Onboarding and signup flows before authentication. +- Unauthenticated web pages redirecting to authenticated mobile apps. +- Authenticated web apps prompting an app download. + +In these cases, you can use a [deep link](https://developer.android.com/training/app-links/deep-linking) on Android and [universal links](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) on iOS to identify users. + +1. Use `posthog.get_distinct_id()` to get the current distinct ID. Even if you cannot call identify because the user is unauthenticated, this will return an anonymous distinct ID generated by PostHog. +2. Add the distinct ID to the deep link as query parameters, along with other properties like UTM parameters. +3. When the user is redirected to the app, parse the deep link and handle the following cases: + +- The mobile app is already authenticated. In this case, call [`posthog.alias()`](/docs/libraries/js/usage.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. +- The mobile app is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/usage.md#identifying-users) with the distinct ID from the web so pre-login mobile events stay connected to the web session. When the user later logs in on mobile, call `identify()` again with your canonical user ID. + +As long as you associate the distinct IDs with `posthog.identify()` or `posthog.alias()`, you can track events generated across platforms. + +Here's an example implementation for handling deep links from web to mobile: + +PostHog AI + +### iOS + +```swift +import PostHog +class DeepLinkIdentityManager { + static let shared = DeepLinkIdentityManager() + // MARK: - Deep Link Received + func handleDeepLink(_ url: URL, isAuthenticatedOnMobile: Bool) { + guard let webDistinctId = URLComponents(url: url, resolvingAgainstBaseURL: true)? + .queryItems?.first(where: { $0.name == "ph_distinct_id" })?.value else { + return + } + if isAuthenticatedOnMobile { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHogSDK.shared.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHogSDK.shared.identify(webDistinctId) + } + } + // MARK: - Login/Signup + func handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHogSDK.shared.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + func handleLogout() { + PostHogSDK.shared.reset() + } +} +``` + +### Android + +```kotlin +import android.net.Uri +import com.posthog.PostHog +object DeepLinkIdentityManager { + // Deep Link Received + fun handleDeepLink(uri: Uri, isAuthenticatedOnMobile: Boolean) { + val webDistinctId = uri.getQueryParameter("ph_distinct_id") ?: return + if (isAuthenticatedOnMobile) { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHog.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHog.identify(webDistinctId) + } + } + // Login/Signup + fun handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHog.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + fun handleLogout() { + PostHog.reset() + } +} +``` + +## Further reading + +- [Identifying users docs](/docs/product-analytics/identify.md) +- [How person processing works](/docs/how-posthog-works/ingestion-pipeline.md#2-person-processing) +- [An introductory guide to identifying users in PostHog](/tutorials/identifying-users-guide.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/flask/flask3-social-media/.env.example b/apps/basic-integration/flask/flask3-social-media/.env.example new file mode 100644 index 000000000..457f26314 --- /dev/null +++ b/apps/basic-integration/flask/flask3-social-media/.env.example @@ -0,0 +1,2 @@ +POSTHOG_PROJECT_TOKEN= +POSTHOG_HOST= diff --git a/apps/basic-integration/flask/flask3-social-media/.venv/include/site/python3.12/greenlet/greenlet.h b/apps/basic-integration/flask/flask3-social-media/.venv/include/site/python3.12/greenlet/greenlet.h new file mode 100644 index 000000000..d02a16e43 --- /dev/null +++ b/apps/basic-integration/flask/flask3-social-media/.venv/include/site/python3.12/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/apps/basic-integration/flask/flask3-social-media/.venv/pyvenv.cfg b/apps/basic-integration/flask/flask3-social-media/.venv/pyvenv.cfg new file mode 100644 index 000000000..608afadbf --- /dev/null +++ b/apps/basic-integration/flask/flask3-social-media/.venv/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3 -m venv /home/runner/work/wizard-workbench/wizard-workbench/apps/basic-integration/flask/flask3-social-media/.venv diff --git a/apps/basic-integration/flask/flask3-social-media/app/__init__.py b/apps/basic-integration/flask/flask3-social-media/app/__init__.py index f15c103dd..a2fe73ad1 100644 --- a/apps/basic-integration/flask/flask3-social-media/app/__init__.py +++ b/apps/basic-integration/flask/flask3-social-media/app/__init__.py @@ -1,7 +1,10 @@ +import atexit import logging from logging.handlers import SMTPHandler, RotatingFileHandler import os -from flask import Flask, request, current_app +from flask import Flask, g, request, current_app +from flask_login import current_user +from posthog import Posthog, identify_context, new_context, set_context_session from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager @@ -33,12 +36,62 @@ def get_locale(): mail = Mail() moment = Moment() babel = Babel() +posthog_client = None def create_app(config_class=Config): + global posthog_client + app = Flask(__name__) app.config.from_object(config_class) + posthog_project_token = app.config['POSTHOG_PROJECT_TOKEN'] + posthog_host = app.config['POSTHOG_HOST'] + for variable_name, value in ( + ('POSTHOG_PROJECT_TOKEN', posthog_project_token), + ('POSTHOG_HOST', posthog_host)): + if not value: + if app.debug: + raise RuntimeError( + f'{variable_name} variable required by PostHog is missing ' + 'or un-configured, this causes events to be silently missed. ' + f'This error stops appearing once {variable_name} is configured') + break + else: + posthog_client = Posthog( + posthog_project_token, + host=posthog_host, + enable_exception_autocapture=True, + ) + atexit.register(posthog_client.shutdown) + + @app.before_request + def bind_posthog_request_context(): + """Bind analytics identity and session context for this request.""" + if posthog_client is None: + return + + context = new_context(fresh=True) + context.__enter__() + g.posthog_context = context + + distinct_id = ( + str(current_user.id) if current_user.is_authenticated + else request.headers.get('X-POSTHOG-DISTINCT-ID') + ) + if distinct_id: + identify_context(distinct_id) + + session_id = request.headers.get('X-POSTHOG-SESSION-ID') + if session_id: + set_context_session(session_id) + + @app.teardown_request + def close_posthog_request_context(exception): + context = g.pop('posthog_context', None) + if context is not None: + context.__exit__(None, None, None) + db.init_app(app) migrate.init_app(app, db) login.init_app(app) diff --git a/apps/basic-integration/flask/flask3-social-media/app/api/auth.py b/apps/basic-integration/flask/flask3-social-media/app/api/auth.py index 82ab26360..7ae488868 100644 --- a/apps/basic-integration/flask/flask3-social-media/app/api/auth.py +++ b/apps/basic-integration/flask/flask3-social-media/app/api/auth.py @@ -1,6 +1,7 @@ import sqlalchemy as sa from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth -from app import db +from app import db, posthog_client +from posthog import identify_context from app.models import User from app.api.errors import error_response @@ -8,10 +9,24 @@ token_auth = HTTPTokenAuth() +def bind_posthog_user(user): + """Bind the API request to its authenticated user.""" + if posthog_client: + identify_context(str(user.id)) + posthog_client.set( + distinct_id=str(user.id), + properties={ + 'email': user.email, + 'username': user.username, + }, + ) + + @basic_auth.verify_password def verify_password(username, password): user = db.session.scalar(sa.select(User).where(User.username == username)) if user and user.check_password(password): + bind_posthog_user(user) return user @@ -22,7 +37,10 @@ def basic_auth_error(status): @token_auth.verify_token def verify_token(token): - return User.check_token(token) if token else None + user = User.check_token(token) if token else None + if user: + bind_posthog_user(user) + return user @token_auth.error_handler diff --git a/apps/basic-integration/flask/flask3-social-media/app/api/tokens.py b/apps/basic-integration/flask/flask3-social-media/app/api/tokens.py index 29a82a40f..353e8ec89 100644 --- a/apps/basic-integration/flask/flask3-social-media/app/api/tokens.py +++ b/apps/basic-integration/flask/flask3-social-media/app/api/tokens.py @@ -1,4 +1,4 @@ -from app import db +from app import db, posthog_client from app.api import bp from app.api.auth import basic_auth, token_auth @@ -8,6 +8,8 @@ def get_token(): token = basic_auth.current_user().get_token() db.session.commit() + if posthog_client: + posthog_client.capture('api_token_issued') return {'token': token} @@ -16,4 +18,6 @@ def get_token(): def revoke_token(): token_auth.current_user().revoke_token() db.session.commit() + if posthog_client: + posthog_client.capture('api_token_revoked') return '', 204 diff --git a/apps/basic-integration/flask/flask3-social-media/app/auth/routes.py b/apps/basic-integration/flask/flask3-social-media/app/auth/routes.py index 8b7bc6717..89bd87ea5 100644 --- a/apps/basic-integration/flask/flask3-social-media/app/auth/routes.py +++ b/apps/basic-integration/flask/flask3-social-media/app/auth/routes.py @@ -3,7 +3,8 @@ from flask_login import login_user, logout_user, current_user from flask_babel import _ import sqlalchemy as sa -from app import db +from app import db, posthog_client +from posthog import identify_context, new_context from app.auth import bp from app.auth.forms import LoginForm, RegistrationForm, \ ResetPasswordRequestForm, ResetPasswordForm @@ -23,6 +24,17 @@ def login(): flash(_('Invalid username or password')) return redirect(url_for('auth.login')) login_user(user, remember=form.remember_me.data) + if posthog_client: + with new_context(fresh=True): + identify_context(str(user.id)) + posthog_client.set( + distinct_id=str(user.id), + properties={ + 'email': user.email, + 'username': user.username, + }, + ) + posthog_client.capture('user_logged_in') next_page = request.args.get('next') if not next_page or urlsplit(next_page).netloc != '': next_page = url_for('main.index') @@ -32,6 +44,8 @@ def login(): @bp.route('/logout') def logout(): + if posthog_client: + posthog_client.capture('user_logged_out') logout_user() return redirect(url_for('main.index')) @@ -46,6 +60,17 @@ def register(): user.set_password(form.password.data) db.session.add(user) db.session.commit() + if posthog_client: + with new_context(fresh=True): + identify_context(str(user.id)) + posthog_client.set( + distinct_id=str(user.id), + properties={ + 'email': user.email, + 'username': user.username, + }, + ) + posthog_client.capture('user_registered') flash(_('Congratulations, your registration is complete!')) return redirect(url_for('auth.login')) return render_template('auth/register.html', title=_('Register'), @@ -62,6 +87,10 @@ def reset_password_request(): sa.select(User).where(User.email == form.email.data)) if user: send_password_reset_email(user) + if posthog_client: + with new_context(fresh=True): + identify_context(str(user.id)) + posthog_client.capture('password_reset_requested') flash( _('Check your email for the instructions to reset your password')) return redirect(url_for('auth.login')) @@ -80,6 +109,10 @@ def reset_password(token): if form.validate_on_submit(): user.set_password(form.password.data) db.session.commit() + if posthog_client: + with new_context(fresh=True): + identify_context(str(user.id)) + posthog_client.capture('password_reset_completed') flash(_('Your password has been reset.')) return redirect(url_for('auth.login')) return render_template('auth/reset_password.html', form=form) diff --git a/apps/basic-integration/flask/flask3-social-media/app/errors/handlers.py b/apps/basic-integration/flask/flask3-social-media/app/errors/handlers.py index 62d42ad83..08098dd88 100644 --- a/apps/basic-integration/flask/flask3-social-media/app/errors/handlers.py +++ b/apps/basic-integration/flask/flask3-social-media/app/errors/handlers.py @@ -1,5 +1,5 @@ from flask import render_template, request -from app import db +from app import db, posthog_client from app.errors import bp from app.api.errors import error_response as api_error_response @@ -19,6 +19,8 @@ def not_found_error(error): @bp.app_errorhandler(500) def internal_error(error): db.session.rollback() + if posthog_client: + posthog_client.capture_exception(error) if wants_json_response(): return api_error_response(500) return render_template('errors/500.html'), 500 diff --git a/apps/basic-integration/flask/flask3-social-media/app/main/routes.py b/apps/basic-integration/flask/flask3-social-media/app/main/routes.py index 103bbc7bc..2854697e6 100644 --- a/apps/basic-integration/flask/flask3-social-media/app/main/routes.py +++ b/apps/basic-integration/flask/flask3-social-media/app/main/routes.py @@ -5,7 +5,7 @@ from flask_babel import _, get_locale import sqlalchemy as sa from langdetect import detect, LangDetectException -from app import db +from app import db, posthog_client from app.main.forms import EditProfileForm, EmptyForm, PostForm, SearchForm, \ MessageForm from app.models import User, Post, Message, Notification @@ -36,6 +36,8 @@ def index(): language=language) db.session.add(post) db.session.commit() + if posthog_client: + posthog_client.capture('post_created') flash(_('Your post is now live!')) return redirect(url_for('main.index')) page = request.args.get('page', 1, type=int) @@ -102,6 +104,8 @@ def edit_profile(): current_user.username = form.username.data current_user.about_me = form.about_me.data db.session.commit() + if posthog_client: + posthog_client.capture('profile_updated') flash(_('Your changes have been saved.')) return redirect(url_for('main.edit_profile')) elif request.method == 'GET': @@ -126,6 +130,8 @@ def follow(username): return redirect(url_for('main.user', username=username)) current_user.follow(user) db.session.commit() + if posthog_client: + posthog_client.capture('user_followed') flash(_('You are following %(username)s!', username=username)) return redirect(url_for('main.user', username=username)) else: @@ -147,6 +153,8 @@ def unfollow(username): return redirect(url_for('main.user', username=username)) current_user.unfollow(user) db.session.commit() + if posthog_client: + posthog_client.capture('user_unfollowed') flash(_('You are not following %(username)s.', username=username)) return redirect(url_for('main.user', username=username)) else: @@ -190,6 +198,8 @@ def send_message(recipient): user.add_notification('unread_message_count', user.unread_message_count()) db.session.commit() + if posthog_client: + posthog_client.capture('message_sent') flash(_('Your message has been sent.')) return redirect(url_for('main.user', username=recipient)) return render_template('send_message.html', title=_('Send Message'), @@ -224,6 +234,8 @@ def export_posts(): else: current_user.launch_task('export_posts', _('Exporting posts...')) db.session.commit() + if posthog_client: + posthog_client.capture('post_export_requested') return redirect(url_for('main.user', username=current_user.username)) diff --git a/apps/basic-integration/flask/flask3-social-media/config.py b/apps/basic-integration/flask/flask3-social-media/config.py index 7e50c9ee7..48e6b2acf 100644 --- a/apps/basic-integration/flask/flask3-social-media/config.py +++ b/apps/basic-integration/flask/flask3-social-media/config.py @@ -23,3 +23,5 @@ class Config: ELASTICSEARCH_URL = os.environ.get('ELASTICSEARCH_URL') REDIS_URL = os.environ.get('REDIS_URL') or 'redis://' POSTS_PER_PAGE = 25 + POSTHOG_PROJECT_TOKEN = os.environ.get('POSTHOG_PROJECT_TOKEN') + POSTHOG_HOST = os.environ.get('POSTHOG_HOST') diff --git a/apps/basic-integration/flask/flask3-social-media/requirements.txt b/apps/basic-integration/flask/flask3-social-media/requirements.txt index 0d986effd..0078de313 100644 --- a/apps/basic-integration/flask/flask3-social-media/requirements.txt +++ b/apps/basic-integration/flask/flask3-social-media/requirements.txt @@ -20,3 +20,4 @@ rq SQLAlchemy Werkzeug WTForms +posthog==7.48.0