
Omnibus Instrument Integration
- 99 installs
- 58 repo stars
- Updated August 5, 2026
- posthog/skills
Helps with ai & agent building tasks during AI-assisted development.
About
omnibus-instrument-integration is a Claude Code skill in the AI & Agent Building category.
- omnibus-instrument-integration
- AI & Agent Building
- AI-coding skill
Omnibus Instrument Integration by the numbers
- 99 all-time installs (skills.sh)
- +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #4,419 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/posthog/skills --skill omnibus-instrument-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 99 |
|---|---|
| repo stars | ★ 58 |
| Last updated | August 5, 2026 |
| Repository | posthog/skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Add PostHog SDK integration
Use this skill to add the PostHog SDK to an application. Use it when setting up PostHog for the first time, or reviewing PRs that need PostHog initialization. Covers SDK installation, provider setup, and basic configuration. Supports any framework or language.
Supported frameworks: Next.js, React, React Router, Vue, Nuxt, TanStack Start, SvelteKit, Astro, Angular, Django, Flask, FastAPI, Laravel, Ruby on Rails, Android, Swift, React Native, Expo, Node.js, and vanilla JavaScript.
Instructions
Follow these steps IN ORDER:
STEP 1: Analyze the codebase and detect the platform.
- Look for dependency files (package.json, requirements.txt, Gemfile, composer.json, go.mod, etc.) to determine the framework and language.
- Look for lockfiles (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lockb) to determine the package manager.
- Check for existing PostHog setup. If PostHog is already installed and initialized, do not modify its code. Inform the user and skip to verification.
STEP 2: Research integration. 2.1. Find the reference file below that matches the detected framework — it is the source of truth for SDK initialization, provider setup, and configuration patterns. Read it now. 2.2. If no reference matches, fall back to your general knowledge and web search. Use posthog.com/docs as the primary search source.
STEP 3: Install the PostHog SDK.
- Add the PostHog SDK package for the detected platform. Do not manually edit package.json — use the package manager's install command.
- Always install packages as a background task. Don't await completion; proceed with other work immediately after starting the installation.
STEP 4: Initialize PostHog.
- Follow the framework reference for where and how to initialize. This varies significantly by framework (e.g., instrumentation-client.ts for Next.js 15.3+, AppConfig.ready() for Django, create_app() for Flask).
- Set up the PostHog provider/wrapper component if the framework requires one.
STEP 5: Identify users.
- Add PostHog
identify()calls on the client side during login and signup events. - If both frontend and backend exist, pass the client-side session and distinct ID using
X-POSTHOG-DISTINCT-IDandX-POSTHOG-SESSION-IDheaders to the server-side code.
STEP 6: Set up environment variables.
- Store the PostHog API key and host in environment variables (e.g.
.envor framework-specific env files). - Reference these environment variables in code instead of hardcoding them.
STEP 7: Verify and clean up.
- Check the project for errors. Look for type checking or build scripts in package.json.
- Ensure any components created were actually used.
- Run any linter or prettier-like scripts found in the package.json.
Reference files
references/EXAMPLE-next-app-router.md- next-app-router example project codereferences/EXAMPLE-next-pages-router.md- next-pages-router example project codereferences/EXAMPLE-react-react-router-6.md- react-react-router-6 example project codereferences/EXAMPLE-react-react-router-7-framework.md- react-react-router-7-framework example project codereferences/EXAMPLE-react-react-router-7-data.md- react-react-router-7-data example project codereferences/EXAMPLE-react-react-router-7-declarative.md- react-react-router-7-declarative example project codereferences/EXAMPLE-react-vite.md- react-vite example project codereferences/EXAMPLE-nuxt-3.6.md- nuxt-3.6 example project codereferences/EXAMPLE-nuxt-4.md- nuxt-4 example project codereferences/EXAMPLE-vue-3.md- vue-3 example project codereferences/EXAMPLE-react-tanstack-router-file-based.md- react-tanstack-router-file-based example project codereferences/EXAMPLE-react-tanstack-router-code-based.md- react-tanstack-router-code-based example project codereferences/EXAMPLE-tanstack-start.md- tanstack-start example project codereferences/EXAMPLE-sveltekit.md- sveltekit example project codereferences/EXAMPLE-astro-static.md- astro-static example project codereferences/EXAMPLE-astro-view-transitions.md- astro-view-transitions example project codereferences/EXAMPLE-astro-ssr.md- astro-ssr example project codereferences/EXAMPLE-astro-hybrid.md- astro-hybrid example project codereferences/EXAMPLE-angular.md- angular example project codereferences/EXAMPLE-javascript-node.md- javascript-node example project codereferences/EXAMPLE-javascript-web.md- javascript-web example project codereferences/EXAMPLE-django.md- django example project codereferences/EXAMPLE-flask.md- flask example project codereferences/EXAMPLE-fastapi.md- fastapi example project codereferences/EXAMPLE-python.md- python example project codereferences/EXAMPLE-laravel.md- laravel example project codereferences/EXAMPLE-ruby-on-rails.md- ruby-on-rails example project codereferences/EXAMPLE-ruby.md- ruby example project codereferences/EXAMPLE-android.md- android example project codereferences/EXAMPLE-swift.md- swift example project codereferences/EXAMPLE-react-native.md- react-native example project codereferences/EXAMPLE-expo.md- expo example project codereferences/next-js.md- Next.js - docsreferences/react.md- React - docsreferences/react-router-v6.md- React router v6 - docsreferences/react-router-v7-framework-mode.md- React router v7 framework mode (remix v3) - docsreferences/react-router-v7-data-mode.md- React router v7 data mode - docsreferences/react-router-v7-declarative-mode.md- React router v7 declarative mode - docsreferences/nuxt-js-3-6.md- Nuxt.js (v3.0 to v3.6) - docsreferences/nuxt-js.md- Nuxt.js - docsreferences/vue-js.md- Vue.js - docsreferences/tanstack-start.md- Tanstack start - docsreferences/svelte.md- Svelte - docsreferences/astro.md- Astro - docsreferences/angular.md- Angular - docsreferences/js.md- JavaScript web - docsreferences/posthog-js.md- PostHog JavaScript web SDKreferences/node.md- Node.js - docsreferences/posthog-node.md- PostHog Node.js SDKreferences/django.md- Django - docsreferences/flask.md- Flask - docsreferences/python.md- Python - docsreferences/posthog-python.md- PostHog python SDKreferences/laravel.md- Laravel - docsreferences/ruby-on-rails.md- Ruby on rails - docsreferences/ruby.md- Ruby - docsreferences/android.md- Android - docsreferences/ios.md- Ios - docsreferences/react-native.md- React native - docsreferences/identify-users.md- Identify users - docs
Each framework reference contains SDK-specific installation, initialization, and usage patterns. Find the one matching the user's stack.
Key principles
- Environment variables: Always use environment variables for PostHog keys. Never hardcode them.
- Minimal changes: Add PostHog code alongside existing integrations. Don't replace or restructure existing code.
- Match the example: Your implementation should follow the example project's patterns as closely as possible.
- Analytics contract: Treat event names, property names, and feature flag keys as part of an analytics contract. Reuse existing names and patterns found in the project. When introducing new ones, make them clear, descriptive, and consistent with existing conventions.
Android - Docs
It uses an internal queue to make calls fast and non-blocking. It also batches requests and flushes asynchronously, making it perfect to use in any part of your mobile app.
Installation
The best way to install the PostHog Android library is with a build system like Gradle. This ensures you can easily upgrade to the latest versions.
All you need to do is add the posthog-android module to your App's build.gradle or build.gradle.kts:
PostHog AI
app/build.gradle
dependencies {
implementation 'com.posthog:posthog-android:3.+'
}app/build.gradle.kts
dependencies {
implementation("com.posthog:posthog-android:3.+")
}Configuration
The best place to initialize the client is in your Application subclass.
Kotlin
PostHog AI
import android.app.Application
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
class SampleApp : Application() {
companion object {
const val POSTHOG_API_KEY = "<ph_project_token>"
// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
const val POSTHOG_HOST = "https://us.i.posthog.com"
}
override fun onCreate() {
super.onCreate()
val config = PostHogAndroidConfig(
apiKey = POSTHOG_API_KEY,
host = POSTHOG_HOST
)
PostHogAndroid.setup(this, config)
}
}Capturing events
You can send custom events using capture:
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.capture(event = "user_signed_up")Tip: We recommend using a[object] [verb]format for your event names, where[object]is the entity that the behavior relates to, and[verb]is the behavior itself. For example,project created,user signed up, orinvite sent.
Setting event properties
Optionally, you can include additional information with the event by including a properties object:
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.capture(
event = "user_signed_up",
properties = mapOf(
"login_type" to "email",
"is_free_trial" to true
)
)Autocapture
PostHog autocapture automatically tracks the following events for you:
- Application Opened - when the app is opened from a closed state or when the app comes to the foreground. (e.g. from the app switcher)
- Deep Link Opened - when the app is opened from a deep link.
- Application Backgrounded - when the app is sent to the background by the user.
- Application Installed - when the app is installed.
- Application Updated - when the app is updated.
- $screen - when the user navigates. (if using
android.app.Activity) - $exception - when the app throws exceptions.
Capturing screen views
With `captureScreenViews = true`, PostHog will try to record all screen changes automatically.
The screenTitle will be the `<activity>`'s android:label, if not set it'll fallback to the `<application>`'s android:label or the `<activity>`'s android:name.
XML
PostHog AI
<activity
android:name="com.example.app.ChildActivity"
android:label="@string/title_child_activity"
...
</activity>If you want to manually send a new screen capture event, use the screen function.
This function requires a screenTitle. You may also pass in an optional properties object.
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.screen(
screenTitle = "Dashboard",
properties = mapOf(
"background" to "blue",
"hero" to "superhog"
)
)Identifying users
We highly recommend reading our section on Identifying users to better understand how to correctly use this method.
Using identify, you can associate events with specific users. This enables you to gain full insights as to how they're using your product across different sessions, devices, and platforms.
An identify call has the following arguments:
- distinctId: Required. A unique identifier for your user. Typically either their email or database ID.
- userProperties: Optional. A dictionary with key:value pairs to set the person properties
- userPropertiesSetOnce: Optional. Similar to
userProperties. See the difference between `userProperties` and `userPropertiesSetOnce`
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.identify(
distinctId = distinctID,
userProperties = mapOf(
"name" to "Max Hedgehog",
"email" to "max@hedgehogmail.com"
),
userPropertiesSetOnce = mapOf(
"date_of_first_log_in" to "2024-03-01"
),
)You should call identify as soon as you're able to. Typically, this is after your user logs in. This ensures that events sent during your user's sessions are correctly associated with them.
When you call identify, all previously tracked anonymous events will be linked to the user.
Get the current user's distinct ID
You may find it helpful to get the current user's distinct ID. For example, to check whether you've already called identify for a user or not.
To do this, call distinctId(). This returns either the ID automatically generated by PostHog or the ID that has been passed by a call to identify().
Alias
Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend.
In this case, you can use alias to assign another distinct ID to the same user.
Kotlin
PostHog AI
/**
* Create an alias for the current user.
*/
PostHog.alias("distinct_id")We strongly recommend reading our docs on alias to best understand how to correctly use this method.
Anonymous and identified events
PostHog captures two types of events: **anonymous** and **identified**
Identified events enable you to attribute events to specific users, and attach person properties. They're best suited for logged-in users.
Scenarios where you want to capture identified events are:
- Tracking logged-in users in B2B and B2C SaaS apps
- Doing user segmented product analysis
- Growth and marketing teams wanting to analyze the complete conversion lifecycle
Anonymous events are events without individually identifiable data. They're best suited for web analytics or apps where users aren't logged in.
Scenarios where you want to capture anonymous events are:
- Tracking a marketing website
- Content-focused sites
- B2C apps where users don't sign up or log in
Under the hood, the key difference between identified and anonymous events is that for identified events we create a person profile for the user, whereas for anonymous events we do not.
Important: Due to the reduced cost of processing them, anonymous events can be up to 4x cheaper than identified ones, so we recommended you only capture identified events when needed.
How to capture anonymous events
The Android SDK captures anonymous events by default. However, this may change depending on your personProfiles config when initializing PostHog:
1. personProfiles = PersonProfiles.IDENTIFIED_ONLY (recommended) (default) - Anonymous events are captured by default. PostHog only captures identified events for users where person profiles have already been created.
2. personProfiles = PersonProfiles.ALWAYS - Capture identified events for all events.
3. personProfiles = PersonProfiles.NEVER - Capture anonymous events for all events.
For example:
Kotlin
PostHog AI
val config = PostHogAndroidConfig(
apiKey = POSTHOG_API_KEY,
host = POSTHOG_HOST,
).apply {
personProfiles = PersonProfiles.IDENTIFIED_ONLY
}How to capture identified events
If you've set the `personProfiles` config to IDENTIFIED_ONLY (the default option), anonymous events are captured by default. Then, to capture identified events, call any of the following functions:
When you call any of these functions, it creates a person profile for the user. Once this profile is created, all subsequent events for this user will be captured as identified events.
Alternatively, you can set personProfiles to ALWAYS to capture identified events by default.
Setting person properties
To set properties on your users via an event, you can leverage the event properties userProperties and userPropertiesSetOnce.
When capturing an event, you can pass a property called userProperties as an event property, and specify its value to be an object with properties to be set on the user that will be associated with the user who triggered the event.
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.capture(
event = "button_b_clicked",
properties = mapOf("color" to "blue"),
userProperties = mapOf(
"string" to "value1",
"integer" to 2
)
)userPropertiesSetOnce works just like userProperties, except that it will only set the property if the user doesn't already have that property set.
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.capture(
event = "button_b_clicked",
properties = mapOf("color" to "blue"),
userPropertiesSetOnce = mapOf(
"string" to "value1",
"integer" to 2
)
)Super Properties
Super Properties are properties associated with events that are set once and then sent with every capture call, be it a $screen, or anything else.
They are set using PostHog.register, which takes a key and value, and they persist across sessions.
For example, take a look at the following call:
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.register("team_id", 22)The call above ensures that every event sent by the user will include "team_id": 22. This way, if you filtered events by property using team_id = 22, it would display all events captured on that user after the PostHog.register call, since they all include the specified Super Property.
However, please note that this does not store properties against the User, only against their events. To store properties against the User object, you should use PostHog.identify. More information on this can be found on the Sending User Information section.
Removing stored Super Properties
Super Properties are persisted across sessions so you have to explicitly remove them if they are no longer relevant. In order to stop sending a Super Property with events, you can use PostHog.unregister, like so:
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.unregister("team_id")This will remove the Super Property and subsequent events will not include it.
If you are doing this as part of a user logging out you can instead simply use PostHog.reset which takes care of clearing all stored Super Properties and more.
Opt out of data capture
You can completely opt-out users from data capture. To do this, there are two options:
1. Opt users out by default by setting optOut to true in your PostHog config:
Kotlin
PostHog AI
val config = PostHogAndroidConfig(
apiKey = <ph_project_token>,
host = https://us.i.posthog.com
)
config.optOut = true
PostHogAndroid.setup(this, config)2. Opt users out on a per-person basis by calling optOut():
Kotlin
PostHog AI
PostHog.optOut()Similarly, you can opt users in:
Kotlin
PostHog AI
PostHog.optIn()To check if a user is opted out:
Kotlin
PostHog AI
PostHog.isOptOut()Flush
You can set the number of events in the configuration that should queue before flushing. Setting this to 1 will send events immediately and will use more battery. The default value for this is 20.
You can also configure the flush interval. By default we flush all events after 30 seconds, no matter how many events have been gathered.
Kotlin
PostHog AI
import com.posthog.android.PostHogAndroidConfig
val config = PostHogAndroidConfig(apiKey = POSTHOG_API_KEY, host = POSTHOG_HOST).apply {
flushAt = 20
flushIntervalSeconds = 30
}You can also manually flush the queue:
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.flush()Reset after logout
To reset the user's ID and anonymous ID, call reset. Usually you would do this right after the user logs out.
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.reset()Feature Flags
PostHog's feature flags enable you to safely deploy and roll back new features as well as target specific users and groups with them.
Boolean feature flags
Kotlin
PostHog AI
import com.posthog.PostHog
if (PostHog.isFeatureEnabled("flag-key")) {
// Do something differently for this user
// Optional: fetch the payload
val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key")
}Multivariate feature flags
Kotlin
PostHog AI
import com.posthog.PostHog
if (PostHog.getFeatureFlag("flag-key") == "variant-key") { // replace 'variant-key' with the key of your variant
// Do something differently for this user
// Optional: fetch the payload
val matchedFlagPayload = PostHog.getFeatureFlagPayload("flag-key")
}Ensuring flags are loaded before usage
Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage.
This means that for most screens, the feature flags are available immediately – except for the first time a user visits.
To handle this, you can use the onFeatureFlags callback to wait for the feature flag request to finish:
Kotlin
PostHog AI
import com.posthog.PostHog
import com.posthog.android.PostHogAndroidConfig
import com.posthog.PostHogOnFeatureFlags
// During SDK initialization
val config = PostHogAndroidConfig(apiKey = "<ph_project_token>").apply {
onFeatureFlags = PostHogOnFeatureFlags {
if (PostHog.isFeatureEnabled("flag-key")) {
// do something
}
}
}
// And/Or manually the SDK is initialized
PostHog.reloadFeatureFlags {
if (PostHog.isFeatureEnabled("flag-key")) {
// do something
}
}Reloading feature flags
Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call:
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.reloadFeatureFlags()Experiments (A/B tests)
Since experiments use feature flags, the code for running an experiment is very similar to the feature flags code:
Kotlin
PostHog AI
import com.posthog.PostHog
if (PostHog.getFeatureFlag("experiment-feature-flag-key") == "variant-name") {
// do something
}It's also possible to run experiments without using feature flags.
Group analytics
Group analytics allows you to associate the events for that person's session with a group (e.g. teams, organizations, etc.). Read the Group Analytics guide for more information.
Note: This is a paid feature and is not available on the open-source or free cloud plan. Learn more on the pricing page.
- Associate the events for this session with a group
Kotlin
PostHog AI
import com.posthog.PostHog
// organization is the group type, company_id_in_your_db is the group ID
PostHog.group(
type = "company",
key = "company_id_in_your_db"
)- Associate the events for this session with a group AND update the properties of that group
Kotlin
PostHog AI
import com.posthog.PostHog
PostHog.group(
type = "company",
key = "company_id_in_your_db",
groupProperties = mapOf("name" to "Awesome Inc.")
)The name is a special property which is used in the PostHog UI for the name of the group. If you don't specify a name property, the group ID will be used instead.
Error tracking
To set up error tracking in your project, follow the Android installation guide.
Session replay
To set up session replay in your project, all you need to do is install the Android SDK, enable "Record user sessions" in your project settings and enable the sessionReplay option.
Offline behavior
The PostHog Android SDK will continue to capture events when the device is offline. The events are stored in a queue in the device's file storage and are flushed when the device is online.
- The queue has a maximum size defined by
maxQueueSizein the configuration. - When the queue is full, the oldest event is deleted first.
- The queue is flushed when the app is restarted and the device is online.
- When you call `flush()` while the device is offline, it aborts early and the events are not flushed.
Debug mode
If you're not seeing the expected events being captured, the feature flags being evaluated, or the surveys being shown, you can enable debug mode to see what's happening.
You can enable debug mode by setting the debug option to true in the PostHogAndroidConfig object. This will enable verbose logs about the inner workings of the SDK.
Kotlin
PostHog AI
val config = PostHogAndroidConfig(apiKey = POSTHOG_API_KEY, host = POSTHOG_HOST).apply {
debug = true
// ... other config options
}All configuration options
When creating the PostHog client, there are many options you can set:
Kotlin
PostHog AI
val config = PostHogAndroidConfig(apiKey = POSTHOG_API_KEY, host = POSTHOG_HOST).apply {
// Capture certain application events automatically. (on/true by default)
captureApplicationLifecycleEvents = true
// Capture screen views automatically. (on/true by default)
captureScreenViews = true // (on/true by default)
// Capture deep links as part of the screen call. (on/true by default)
captureDeepLinks = true
// Maximum number of events to keep in queue before flushing (20 by default)
flushAt = 20
// Number of maximum events in memory and disk, when the maximum is exceed, the oldest event is deleted and the new one takes place. (1000 by default)
maxQueueSize = 1000
// Number of maximum events in a batch call. (50 by default)
maxBatchSize = 50
// Maximum delay before flushing the queue (30 seconds)
flushIntervalSeconds = 30
// Logs the SDK messages into Logcat. (off/false by default)
debug = false
// Prevents capturing any data if enabled. (off/false by default)
optOut = false
// Send a '$feature_flag_called' event when a feature flag is used automatically. (on/true by default)
sendFeatureFlagEvent = true
// Preload feature flags automatically. (on/true by default)
preloadFeatureFlags = true
// Evaluation context tags that constrain which feature flags are evaluated. (not set by default)
// When set, only flags with matching evaluation context tags (or no evaluation context tags) will be returned.
// Available in version 3.25.0+. The legacy parameter `evaluationEnvironments` (version 3.24.0+) is also supported.
evaluationContexts = listOf("production", "android", "mobile")
// Callback that is called when feature flags are loaded (not set by default)
onFeatureFlags = { ... }
// Callback that allows to sanitize the event properties (not set by default)
propertiesSanitizer = { properties -> ... }
// Hook for encrypt and decrypt events
// Devices are sandbox already
// Defaults to no encryption
encryption = object : PostHogEncryption { ... }
// Hook that allows for modification of the default mechanism for
// generating anonymous id (which as of now is just random UUID v7)
getAnonymousId = { ... }
// Determines the behavior for processing user profiles.
// Defaults to PersonProfiles.IDENTIFIED_ONLY
personProfiles = PersonProfiles.IDENTIFIED_ONLY
// Enable Recording of Session Replay. (off/false by default)
sessionReplay = false
// Session Replay configuration
// https://posthog.com/docs/session-replay/installation for more details
sessionReplayConfig = PostHogSessionReplayConfig(...)
// Whether the SDK should reuse the anonymous Id between user changes.
// When enabled, a single Id will be used for all anonymous users on this device (off/false by default)
reuseAnonymousId = false
// Error tracking configuration
errorTrackingConfig = PostHogErrorTrackingConfig(...)
}FAQ
Do I need to declare permissions in the AndroidManifest.xml?
We don't declare nor use any 'Service', so no permissions are needed.
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Angular - Docs
PostHog makes it easy to get data about traffic and usage of your Angular app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
This guide walks you through integrating PostHog into your Angular app using the JavaScript Web SDK.
Installation
Install posthog-js using your package manager:
PostHog AI
npm
npm install --save posthog-jsYarn
yarn add posthog-jspnpm
pnpm add posthog-jsBun
bun add posthog-jsInitialize the PostHog client
Generate environment files for your project with ng g environments. Configure the following environment variables:
-
posthogKey: Your project token from your project settings. -
posthogHost: Your project's client API host. Usuallyhttps://us.i.posthog.comfor US-based projects andhttps://eu.i.posthog.comfor EU-based projects.
Angular v17+
For Angular v17 and above, you can set up PostHog as a singleton service. To do this, start by creating and injecting a PosthogService instance.
Create a service by running ng g service services/posthog. The service should look like this:
posthog.service.ts
PostHog AI
// src/app/services/posthog.service.ts
import { DestroyRef, Injectable, NgZone } from "@angular/core";
import posthog from "posthog-js";
import { environment } from "../../environments/environment";
import { Router } from "@angular/router";
@Injectable({ providedIn: "root" })
export class PosthogService {
constructor(
private ngZone: NgZone,
private router: Router,
private destroyRef: DestroyRef,
) {
this.initPostHog();
}
private initPostHog() {
this.ngZone.runOutsideAngular(() => {
posthog.init(environment.posthogKey, {
api_host: environment.posthogHost,
defaults: '2026-01-30',
});
});
}
}The service is initialized outside of the Angular zone to reduce change detection cycles. This is important to avoid performance issues with session recording.
Then, inject the service in your app's root component app.component.ts. This will make sure PostHog is initialized before any other component is rendered.
app.component.ts
PostHog AI
// src/app/app.component.ts
import { Component } from "@angular/core";
import { RouterOutlet } from "@angular/router";
import { PosthogService } from "./services/posthog.service";
@Component({
selector: "app-root",
styleUrls: ["./app.component.scss"],
template: `
<router-outlet />`,
imports: [RouterOutlet],
})
export class AppComponent {
title = "angular-app";
constructor(posthogService: PosthogService) {}
}Angular v16 and below
In your src/main.ts, initialize PostHog using your project token and instance address. You can find both in your project settings.
main.ts
PostHog AI
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { environment } from "./environments/environment";
import posthog from 'posthog-js'
posthog.init(environment.posthogKey, {
api_host: environment.posthogHost,
defaults: '2026-01-30'
})
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));Identifying users
Identifying users is required. Call posthog.identify('your-user-id') after login to link events to a known user. This is what connects frontend event captures, session replays, LLM traces, and error tracking to the same person — and lets backend events link back too.>
See our guide on identifying users for how to set this up.
Note: If you're using Typescript, you might have some trouble getting your types to compile because we depend onrrwebbut don't ship all of their types. To accommodate that, you'll need to add@rrweb/types@2.0.0-alpha.17andrrweb-snapshot@2.0.0-alpha.17as a dependency if you want your Angular compiler to typecheck correctly.
>
Given the nature of this library, you might need to completely clear your .npm cache to get this to work as expected. Make sure your clear your CI's cache as well.>
In the rare case the versions above get out-of-date, you can check our JavaScript SDK's `package.json` to understand what's the exact version you need to depend on.
Set up a reverse proxy (recommended)
We recommend setting up a reverse proxy, so that events are less likely to be intercepted by tracking blockers.
We have our own managed reverse proxy service, which is free for all PostHog Cloud users, routes through our infrastructure, and makes setting up your proxy easy.
If you don't want to use our managed service then there are several other options for creating a reverse proxy, including using Cloudflare, AWS Cloudfront, and Vercel.
Grouping products in one project (recommended)
If you have multiple customer-facing products (e.g. a marketing website + mobile app + web app), it's best to install PostHog on them all and group them in one project.
This makes it possible to track users across their entire journey (e.g. from visiting your marketing website to signing up for your product), or how they use your product across multiple platforms.
Add IPs to Firewall/WAF allowlists (recommended)
For certain features like heatmaps, your Web Application Firewall (WAF) may be blocking PostHog’s requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site.
EU: 3.75.65.221, 18.197.246.42, 3.120.223.253
US: 44.205.89.55, 52.4.194.122, 44.208.188.173
These are public, stable IPs used by PostHog services (e.g., Celery tasks for snapshots).
Tracking pageviews
PostHog automatically tracks your pageviews by hooking up to the browser's navigator API as long as you initialize PostHog with the defaults config option set after 2026-01-30.
Capture custom events
To capture custom events, import posthog and call posthog.capture(). Below is an example of how to do this in a component:
app.component.ts
PostHog AI
import { Component } from '@angular/core';
import posthog from 'posthog-js'
@Component({
// existing component code
})
export class AppComponent {
handleClick() {
posthog.capture(
'home_button_clicked',
)
}
}Session replay
Session replay uses change detection to record the DOM. This can clash with Angular's change detection.
The recorder tool attempts to detect when an Angular zone is present and avoid the clash but might not always succeed.
- If you followed the installation instructions for Angular v17 and above, you don't need to do anything.
- If you followed the installation instructions for Angular v16 and below and you see performance impact from recording in an Angular project, ensure that you use `ngZone.runOutsideAngular`.
posthog.service.ts
PostHog AI
import { Injectable } from '@angular/core';
import posthog from 'posthog-js'
@Injectable({ providedIn: 'root' })
export class PostHogSessionRecordingService {
constructor(private ngZone: NgZone) {}
initPostHog() {
this.ngZone.runOutsideAngular(() => {
posthog.init(
/* your config */
)
})
}
}Angular with SSR
To use PostHog with Angular server-side rendering (SSR), you need to:
1. Update the PostHog web JS client to only initialize on the client-side. 2. Initialize PostHog Node on the server-side.
1\. Update the PostHog web JS client
Update your posthog.service.ts to restrict the initialization of the PostHog web JS client to the client-side. The web SDK uses methods that are not available on the server side, so we need to check if we're on the client side before initializing PostHog.
posthog.service.ts
PostHog AI
import { isPlatformBrowser } from "@angular/common";
import { PLATFORM_ID } from "@angular/core";
import { isPlatformBrowser } from "@angular/common";
import { PLATFORM_ID } from "@angular/core";
@Injectable({ providedIn: "root" })
export class PosthogService {
constructor(
private ngZone: NgZone,
private router: Router,
private destroyRef: DestroyRef,
@Inject(PLATFORM_ID) private platformId: Object
) {
// Only initialize PostHog in browser environment
if (isPlatformBrowser(this.platformId)) {
this.initPostHog(); //+
}2\. Add server-side initialization
Angular SSR uses a server.ts file to handle requests. We can add any server-side initialization code to this file.
First, install the posthog-node package to run on the server side.
PostHog AI
npm
npm install posthog-node --saveYarn
yarn add posthog-nodepnpm
pnpm add posthog-nodeBun
bun add posthog-nodeThen, add the following code to the server.ts file:
server.ts
PostHog AI
// src/server.ts
import { environment } from './environments/environment';
import { PostHog } from 'posthog-node'
/**
* Extract distinct ID from PostHog cookie
*/
function getDistinctIdFromCookie(cookieHeader: string | undefined): string | null {
if (!cookieHeader) return null;
const cookieMatch = cookieHeader.match(`ph_${environment.posthogKey}_posthog=([^;]+)`);
if (cookieMatch) {
try {
const parsed = JSON.parse(decodeURIComponent(cookieMatch[1]));
return parsed?.distinct_id || null;
} catch (error) {
console.error('Error parsing PostHog cookie:', error);
return null;
}
}
return null;
}
/**
* Handle all other requests by rendering the Angular application.
*/
app.get('**', async (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
const distinctId = getDistinctIdFromCookie(headers.cookie);
let isFeatureEnabled = false;
const client = new PostHog(
const client = new PostHog(
environment.posthogKey,
{ host: environment.posthogHost }
)
)
if (distinctId) {
client.capture({
distinctId: distinctId,
event: 'test_ssr_event',
properties: {
message: 'Hello from Angular SSR!'
}
})
isFeatureEnabled = await client.isFeatureEnabled(
'your_feature_flag_key', distinctId) || false;
}
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [
{ provide: APP_BASE_HREF, useValue: baseUrl },
{ provide: 'FEATURE_FLAG_ENABLED', useValue: isFeatureEnabled }
],
})
.then((html) => res.send(html))
.catch((err) => next(err));
await client.shutdown()
});This code does the following:
- Extracts the distinct ID from the cookie header. This is set by the web JS client.
- Captures an event on the server side.
- Evaluates a feature flag on the server side. This can be passed as a provider to the Angular application.
- Calls
shutdownon the PostHog Node client to ensure all events are flushed.
Using PostHog in server-side code
Angular SSR does not allow Node.js code to be bundled into client-side components. Even though resolvers and other server-side code can be written along with client-side components, you cannot use PostHog Node in those components.
Next steps
For any technical questions for how to integrate specific PostHog features into Angular (such as feature flags, A/B testing, surveys, etc.), have a look at our JavaScript Web SDK docs.
Alternatively, the following tutorials can help you get started:
- How to set up Angular analytics, feature flags, and more
- How to set up A/B tests in Angular
- How to set up surveys in Angular
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Astro - Docs
PostHog makes it easy to get data about traffic and usage of your Astro app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
This guide walks you through integrating PostHog into your Astro app using the JavaScript Web SDK.
Beta: integration via LLM
Install PostHog for Astro in seconds with our wizard by running this prompt with LLM coding agents like Cursor and Bolt, or by running it in your terminal.
npx @posthog/wizard@latest
Or, to integrate manually, continue with the rest of this guide.
Installation
In your src/components folder, create a posthog.astro file:
Terminal
PostHog AI
cd ./src/components
# or 'cd ./src && mkdir components && cd ./components' if your components folder doesnt exist
touch posthog.astroIn this file, add your Web snippet which you can find in your project settings. Be sure to include the is:inline directive to prevent Astro from processing it, or you will get Typescript and build errors that property 'posthog' does not exist on type 'Window & typeof globalThis'.
posthog.astro
PostHog AI
<script is:inline>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('<ph_project_token>', {
api_host:'https://us.i.posthog.com',
defaults: '2026-01-30'
})
</script>Using with Astro's view transitions (ClientRouter)
If you've opted in to Astro's <ClientRouter> component for client-side navigation, you'll need to add an initialization guard to prevent PostHog from running multiple times during page transitions.
Update your posthog.astro file to wrap the snippet with a check:
posthog.astro
PostHog AI
---
// src/components/posthog.astro
---
<script is:inline>
if (!window.__posthog_initialized) {
window.__posthog_initialized = true;
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('<ph_project_token>', {
api_host: 'https://us.i.posthog.com',
defaults: '2026-01-30',
capture_pageview: 'history_change'
})
}
</script>Without this guard, ClientRouter's soft navigation can re-execute the inline script during page transitions, causing a stack overflow error. The capture_pageview: 'history_change' option ensures pageviews are tracked automatically as users navigate.
The next step is to a create a Layout where we will use posthog.astro. Create a new file PostHogLayout.astro in your src/layouts folder:
Terminal
PostHog AI
cd .. && cd .. # move back to your base directory if you're still in src/components/posthog.astro
cd ./src/layouts
# or 'cd ./src && mkdir layouts && cd ./layouts' if your layouts folder doesn't exist yet
touch PostHogLayout.astroAdd the following code to PostHogLayout.astro:
PostHogLayout.astro
PostHog AI
---
import PostHog from '../components/posthog.astro'
---
<head>
<PostHog />
</head>Lastly, update index.astro to wrap your existing app components with the new Layout:
index.astro
PostHog AI
---
import PostHogLayout from '../layouts/PostHogLayout.astro';
---
<PostHogLayout>
<!-- your existing app components -->
</PostHogLayout>Identifying users
Identifying users is required. Call posthog.identify('your-user-id') after login to link events to a known user. This is what connects frontend event captures, session replays, LLM traces, and error tracking to the same person — and lets backend events link back too.>
See our guide on identifying users for how to set this up.
Set up a reverse proxy (recommended)
We recommend setting up a reverse proxy, so that events are less likely to be intercepted by tracking blockers.
We have our own managed reverse proxy service, which is free for all PostHog Cloud users, routes through our infrastructure, and makes setting up your proxy easy.
If you don't want to use our managed service then there are several other options for creating a reverse proxy, including using Cloudflare, AWS Cloudfront, and Vercel.
Grouping products in one project (recommended)
If you have multiple customer-facing products (e.g. a marketing website + mobile app + web app), it's best to install PostHog on them all and group them in one project.
This makes it possible to track users across their entire journey (e.g. from visiting your marketing website to signing up for your product), or how they use your product across multiple platforms.
Add IPs to Firewall/WAF allowlists (recommended)
For certain features like heatmaps, your Web Application Firewall (WAF) may be blocking PostHog’s requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site.
EU: 3.75.65.221, 18.197.246.42, 3.120.223.253
US: 44.205.89.55, 52.4.194.122, 44.208.188.173
These are public, stable IPs used by PostHog services (e.g., Celery tasks for snapshots).
Next steps
For any technical questions for how to integrate specific PostHog features into Astro (such as analytics, feature flags, A/B testing, surveys, etc.), have a look at our JavaScript Web SDK docs.
Alternatively, the following tutorials can help you get started:
- How to set up Astro analytics, feature flags, and more
- How to set up A/B tests in Astro
- How to set up surveys in Astro
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Django - Docs
PostHog makes it easy to get data about traffic and usage of your Django app. Integrating PostHog enables analytics, custom events capture, feature flags, and more.
This guide walks you through integrating PostHog into your Django app using the Python SDK.
Beta: integration via LLM
Install PostHog for Django in seconds with our wizard by running this prompt with LLM coding agents like Cursor and Bolt, or by running it in your terminal.
npx @posthog/wizard@latest
Or, to integrate manually, continue with the rest of this guide.
Installation
To start, run pip install posthog to install PostHog’s Python SDK.
Then, set the PostHog API key and host in your AppConfig in your your_app/apps.py so that's it's available everywhere:
your\_app/apps.py
PostHog AI
from django.apps import AppConfig
import posthog
class YourAppConfig(AppConfig):
name = "your_app_name"
def ready(self):
posthog.api_key = '<ph_project_token>'
posthog.host = 'https://us.i.posthog.com'You can find your project token and instance address in your project settings.
Next, if you haven't done so already, make sure you add your AppConfig to your settings.py under INSTALLED_APPS:
settings.py
PostHog AI
INSTALLED_APPS = [
# other apps
'your_app_name.apps.MyAppConfig', # Add your app config
]Lastly, to access PostHog in any file, simply import posthog and call the method you'd like. For example, to capture an event:
Python
PostHog AI
import posthog
def some_request(request):
with posthog.new_context():
posthog.identify_context(request.user.id)
posthog.capture('event_name')Identifying users
Identifying users is required. Backend events need adistinct_idthat matches the ID your frontend uses when callingposthog.identify(). Without this, backend events are orphaned — they can't be linked to frontend event captures, session replays, LLM traces, or error tracking.
>
See our guide on identifying users for how to set this up.
Django contexts middleware
The Python SDK provides a Django middleware that automatically wraps all requests with a context. This middleware extracts session and user information from request headers and tags all events captured during the request with relevant metadata.
Basic setup
Add the middleware to your Django settings:
Python
PostHog AI
MIDDLEWARE = [
# ... other middleware
'posthog.integrations.django.PosthogContextMiddleware',
# ... other middleware
]The middleware automatically extracts and uses:
- Session ID from the
X-POSTHOG-SESSION-IDheader, if present - Distinct ID from the
X-POSTHOG-DISTINCT-IDheader, if present - Current URL as
$current_url - Request method as
$request_method
All events captured during the request (including exceptions) will include these properties and be associated with the extracted session and distinct ID.
If you are using PostHog on your frontend, the JavaScript Web SDK will add the session and distinct ID headers automatically if you enable tracing headers.
JavaScript
PostHog AI
posthog.init('<ph_project_token>', {
__add_tracing_headers: ['your-backend-domain.com']
})Exception capture
By default, the middleware captures exceptions and sends them to PostHog's error tracking. Disable this by setting:
Python
PostHog AI
# settings.py
POSTHOG_MW_CAPTURE_EXCEPTIONS = FalseAdding custom tags
Use POSTHOG_MW_EXTRA_TAGS to add custom properties to all requests:
Python
PostHog AI
# settings.py
def add_user_tags(request):
# type: (HttpRequest) -> Dict[str, Any]
tags = {}
if hasattr(request, 'user') and request.user.is_authenticated:
tags['user_id'] = request.user.id
tags['email'] = request.user.email
return tags
POSTHOG_MW_EXTRA_TAGS = add_user_tagsFiltering requests
Skip tracking for certain requests using POSTHOG_MW_REQUEST_FILTER:
Python
PostHog AI
# settings.py
def should_track_request(request):
# type: (HttpRequest) -> bool
# Don't track health checks or admin requests
if request.path.startswith('/health') or request.path.startswith('/admin'):
return False
return True
POSTHOG_MW_REQUEST_FILTER = should_track_requestModifying default tags
Use POSTHOG_MW_TAG_MAP to modify or remove default tags:
Python
PostHog AI
# settings.py
def customize_tags(tags):
# type: (Dict[str, Any]) -> Dict[str, Any]
# Remove URL for privacy
tags.pop('$current_url', None)
# Add custom prefix to method
if '$request_method' in tags:
tags['http_method'] = tags.pop('$request_method')
return tags
POSTHOG_MW_TAG_MAP = customize_tagsComplete configuration example
Python
PostHog AI
# settings.py
def add_request_context(request):
# type: (HttpRequest) -> Dict[str, Any]
tags = {}
if hasattr(request, 'user') and request.user.is_authenticated:
tags['user_type'] = 'authenticated'
tags['user_id'] = str(request.user.id)
else:
tags['user_type'] = 'anonymous'
# Add request info
tags['user_agent'] = request.META.get('HTTP_USER_AGENT', '')
return tags
def filter_tracking(request):
# type: (HttpRequest) -> bool
# Skip internal endpoints
return not request.path.startswith(('/health', '/metrics', '/admin'))
def clean_tags(tags):
# type: (Dict[str, Any]) -> Dict[str, Any]
# Remove sensitive data
tags.pop('user_agent', None)
return tags
POSTHOG_MW_EXTRA_TAGS = add_request_context
POSTHOG_MW_REQUEST_FILTER = filter_tracking
POSTHOG_MW_TAG_MAP = clean_tags
POSTHOG_MW_CAPTURE_EXCEPTIONS = TrueAll events captured within the request context automatically include the configured tags and are associated with the session and user identified from the request headers.
Next steps
For any technical questions for how to integrate specific PostHog features into Django (such as analytics, feature flags, A/B testing, etc.), have a look at our Python SDK docs.
Alternatively, the following tutorials can help you get started:
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
PostHog android Example Project
Repository: https://github.com/PostHog/context-mill Path: basics/android
---
README.md
PostHog Android example
This is an Android example demonstrating PostHog integration with product analytics, session replay, and error tracking using Kotlin and Jetpack Compose.
This example uses the PostHog Android SDK (posthog-android) to provide automatic PostHog integration with built-in error tracking, session replay, and simplified configuration.
Features
- Product Analytics: Track user events and behaviors
- Session Replay: Record and replay user sessions
- Error Tracking: Automatic error capture and crash reporting
- User Authentication: Demo login system with PostHog user identification
- Event Tracking: Examples of custom event tracking throughout the app
Getting Started
1. Prerequisites
- Android Studio (latest stable version)
- Android SDK (API level 24 or higher)
- JDK 11 or higher
- Gradle 8.0 or higher
- A PostHog account
2. Configure Environment Variables
The PostHog configuration is stored in local.properties (this file is gitignored):
# PostHog configuration
posthog.apiKey=your_posthog_project_token
posthog.host=https://us.i.posthog.comAlternatively, you can configure PostHog in your build.gradle file:
android {
defaultConfig {
buildConfigField "String", "POSTHOG_PROJECT_TOKEN", "\"your_posthog_project_token\""
buildConfigField "String", "POSTHOG_HOST", "\"https://us.i.posthog.com\""
}
}Get your PostHog project token from your PostHog project settings.
3. Build and Run
1. Open the project in Android Studio 2. Sync Gradle files 3. Run the app on an emulator or physical device
Project Structure
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/example/posthog/
│ │ │ │ ├── BurritoApplication.kt # Application class with PostHog initialization
│ │ │ │ ├── MainActivity.kt # Main activity
│ │ │ │ ├── ui/
│ │ │ │ │ ├── screens/
│ │ │ │ │ │ ├── LoginScreen.kt # Login screen with user identification
│ │ │ │ │ │ ├── BurritoScreen.kt # Demo feature screen with event tracking
│ │ │ │ │ │ └── ProfileScreen.kt # User profile with error tracking demo
│ │ │ │ │ └── components/ # Reusable UI components
│ │ │ │ └── utils/
│ │ │ │ └── PostHogHelper.kt # PostHog utility functions
│ │ │ ├── res/ # Resources (layouts, strings, etc.)
│ │ │ └── AndroidManifest.xml # App manifest
│ │ └── test/ # Unit tests
│ └── build.gradle # App-level Gradle configuration
├── build.gradle # Project-level Gradle configuration
├── settings.gradle # Gradle settings
└── local.properties # Local configuration (gitignored)Key Integration Points
Application Initialization (BurritoApplication.kt)
PostHog is initialized in the Application class to ensure it's available throughout the app lifecycle:
class BurritoApplication : Application() {
override fun onCreate() {
super.onCreate()
val posthogConfig = PostHogConfig(
apiKey = BuildConfig.POSTHOG_PROJECT_TOKEN,
host = BuildConfig.POSTHOG_HOST
).apply {
// Enable session replay
sessionReplay = true
// Enable automatic exception capture
captureApplicationLifecycleEvents = true
captureDeepLinks = true
captureScreenViews = true
}
PostHog.setup(this, posthogConfig)
}
}Key Points:
- PostHog is initialized in
onCreate()to ensure it's initialized as early as possible - Configuration is loaded from
BuildConfig(set inbuild.gradle) - Session replay, lifecycle events, and screen views are enabled
- The Application class must be registered in
AndroidManifest.xml
User Identification (LoginScreen.kt)
Users are identified when they log in:
val posthog = PostHog.getInstance()
fun handleLogin(username: String, password: String) {
// Authenticate user
val success = authenticateUser(username, password)
if (success) {
// Identify the user once on login/sign up
posthog.identify(
distinctId = username,
properties = mapOf(
"username" to username,
"login_method" to "password"
)
)
// Capture login event
posthog.capture("user_logged_in", mapOf(
"username" to username
))
}
}Key Points:
identify()is called once when the user logs in or signs up- User properties can be set during identification
- Events are captured using
capture()with event names and properties - The
distinctIdshould be a unique identifier for the user
Event Tracking (BurritoScreen.kt)
Custom events are tracked throughout the app:
val posthog = PostHog.getInstance()
fun handleBurritoConsideration() {
// Track custom event
posthog.capture("burrito_considered", mapOf(
"total_considerations" to considerationCount,
"username" to currentUser.username,
"timestamp" to System.currentTimeMillis()
))
// Update user properties
posthog.setUserProperties(mapOf(
"last_burrito_consideration" to System.currentTimeMillis(),
"total_burrito_considerations" to considerationCount
))
}Key Points:
- Events are captured with
capture()method - Event properties provide context about the event
- User properties can be updated with
setUserProperties() - Properties can be strings, numbers, booleans, or dates
Error Tracking
Errors are captured automatically and can also be tracked manually:
Automatic Error Capture: PostHog automatically captures uncaught exceptions when configured:
val posthogConfig = PostHogConfig(
apiKey = BuildConfig.POSTHOG_PROJECT_TOKEN,
host = BuildConfig.POSTHOG_HOST
).apply {
// Automatic exception capture is enabled by default
captureApplicationLifecycleEvents = true
}Manual Error Capture:
val posthog = PostHog.getInstance()
try {
// Risky operation
performRiskyOperation()
} catch (e: Exception) {
// Capture exception manually
posthog.captureException(e, mapOf(
"context" to "burrito_consideration",
"user_id" to currentUser.id
))
}Screen View Tracking
Screen views are automatically tracked when captureScreenViews is enabled. You can also manually track screen views:
val posthog = PostHog.getInstance()
// Manual screen view tracking
posthog.screen("BurritoScreen", mapOf(
"screen_category" to "features",
"user_type" to "premium"
))Session Replay
Session replay is enabled in the PostHog configuration:
val posthogConfig = PostHogConfig(
apiKey = BuildConfig.POSTHOG_PROJECT_TOKEN,
host = BuildConfig.POSTHOG_HOST
).apply {
sessionReplay = true
sessionReplayConfig = SessionReplayConfig(
maskAllInputs = false, // Set to true to mask all input fields
maskAllText = false // Set to true to mask all text
)
}Accessing PostHog in Components
PostHog is accessed via the singleton instance:
val posthog = PostHog.getInstance()
posthog.capture("event_name", mapOf("property" to "value"))The instance is available throughout your application after initialization.
Gradle Configuration
App-level build.gradle
android {
defaultConfig {
// PostHog configuration
buildConfigField "String", "POSTHOG_PROJECT_TOKEN", "\"${project.findProperty("posthog.apiKey") ?: ""}\""
buildConfigField "String", "POSTHOG_HOST", "\"${project.findProperty("posthog.host") ?: "https://us.i.posthog.com"}\""
}
}
dependencies {
// PostHog Android SDK
implementation 'com.posthog:posthog-android:3.+'
// Other dependencies...
}Reading from local.properties
The local.properties file is automatically read by Gradle:
def localProperties = new Properties()
localProperties.load(new FileInputStream(rootProject.file("local.properties")))
android {
defaultConfig {
buildConfigField "String", "POSTHOG_PROJECT_TOKEN", "\"${localProperties.getProperty("posthog.apiKey", "")}\""
buildConfigField "String", "POSTHOG_HOST", "\"${localProperties.getProperty("posthog.host", "https://us.i.posthog.com")}\""
}
}Best Practices
1. Initialize Early: Initialize PostHog in your Application.onCreate() method 2. Identify Once: Call identify() once when the user logs in or signs up 3. Use Meaningful Event Names: Use clear, descriptive event names (e.g., user_logged_in instead of login) 4. Include Context: Add relevant properties to events for better analysis 5. Handle Errors Gracefully: Don't let PostHog errors break your app 6. Test in Development: Use a separate PostHog project for development/testing 7. Respect Privacy: Be mindful of PII (Personally Identifiable Information) in events and properties
Learn More
---
app/src/main/java/com/example/posthog/BurritoApp.kt
package com.example.posthog
import android.app.Application
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
class BurritoApplication : Application() {
override fun onCreate() {
super.onCreate()
// Initialize PostHog early in Application lifecycle
val config = PostHogAndroidConfig(
apiKey = BuildConfig.POSTHOG_PROJECT_TOKEN,
host = BuildConfig.POSTHOG_HOST,
).apply {
debug = true
errorTrackingConfig.autoCapture = true
}
PostHogAndroid.setup(this, config)
}
}
---
app/src/main/java/com/example/posthog/data/User.kt
package com.example.posthog.data
data class User(
val username: String,
val burritoConsiderations: Int = 0
)
---
app/src/main/java/com/example/posthog/data/UserRepository.kt
package com.example.posthog.data
import android.content.Context
import android.content.SharedPreferences
import org.json.JSONObject
class UserRepository(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences(
PREFS_NAME, Context.MODE_PRIVATE
)
companion object {
private const val PREFS_NAME = "burrito_app_prefs"
private const val KEY_CURRENT_USERNAME = "current_username"
private const val KEY_USER_DATA_PREFIX = "user_data_"
}
fun getCurrentUsername(): String? {
return prefs.getString(KEY_CURRENT_USERNAME, null)
}
fun getUser(username: String): User? {
val json = prefs.getString("$KEY_USER_DATA_PREFIX$username", null) ?: return null
return try {
val obj = JSONObject(json)
User(
username = obj.getString("username"),
burritoConsiderations = obj.getInt("burritoConsiderations")
)
} catch (e: Exception) {
null
}
}
fun saveUser(user: User) {
val json = JSONObject().apply {
put("username", user.username)
put("burritoConsiderations", user.burritoConsiderations)
}.toString()
prefs.edit()
.putString("$KEY_USER_DATA_PREFIX${user.username}", json)
.putString(KEY_CURRENT_USERNAME, user.username)
.apply()
}
fun clearCurrentUser() {
prefs.edit()
.remove(KEY_CURRENT_USERNAME)
.apply()
}
fun getCurrentUser(): User? {
val username = getCurrentUsername() ?: return null
return getUser(username)
}
}
---
app/src/main/java/com/example/posthog/MainActivity.kt
package com.example.posthog
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.example.posthog.navigation.NavGraph
import com.example.posthog.navigation.Screen
import com.example.posthog.ui.components.AppHeader
import com.example.posthog.ui.components.BottomNavBar
import com.example.posthog.ui.theme.BackgroundGray
import com.example.posthog.ui.theme.PostHogTheme
import com.example.posthog.viewmodel.AuthViewModel
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
PostHogTheme {
BurritoApp()
}
}
}
}
@Composable
fun BurritoApp() {
val navController = rememberNavController()
val viewModel: AuthViewModel = viewModel()
val isAuthenticated by viewModel.isAuthenticated.collectAsState()
val currentUser by viewModel.currentUser.collectAsState()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
AppHeader(
isAuthenticated = isAuthenticated,
username = currentUser?.username,
currentRoute = currentRoute,
onNavigate = { route ->
navController.navigate(route) {
popUpTo(Screen.Home.route)
launchSingleTop = true
}
},
onLogout = {
viewModel.logout()
navController.navigate(Screen.Home.route) {
popUpTo(Screen.Home.route) { inclusive = true }
}
}
)
},
bottomBar = {
BottomNavBar(
isAuthenticated = isAuthenticated,
currentRoute = currentRoute,
onNavigate = { route ->
navController.navigate(route) {
popUpTo(Screen.Home.route)
launchSingleTop = true
}
}
)
},
containerColor = BackgroundGray
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.background(BackgroundGray)
.padding(innerPadding)
) {
NavGraph(
navController = navController,
viewModel = viewModel
)
}
}
}
---
app/src/main/java/com/example/posthog/navigation/NavGraph.kt
package com.example.posthog.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.example.posthog.ui.screens.BurritoScreen
import com.example.posthog.ui.screens.HomeScreen
import com.example.posthog.ui.screens.ProfileScreen
import com.example.posthog.viewmodel.AuthViewModel
sealed class Screen(val route: String) {
object Home : Screen("home")
object Burrito : Screen("burrito")
object Profile : Screen("profile")
}
@Composable
fun NavGraph(
navController: NavHostController,
viewModel: AuthViewModel
) {
val isAuthenticated by viewModel.isAuthenticated.collectAsState()
val currentUser by viewModel.currentUser.collectAsState()
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen(
isAuthenticated = isAuthenticated,
username = currentUser?.username,
onLogin = { username -> viewModel.login(username) }
)
}
composable(Screen.Burrito.route) {
if (!isAuthenticated) {
LaunchedEffect(Unit) {
navController.navigate(Screen.Home.route) {
popUpTo(Screen.Home.route) { inclusive = true }
}
}
} else {
BurritoScreen(
burritoCount = currentUser?.burritoConsiderations ?: 0,
onConsiderBurrito = { viewModel.incrementBurritoCount() }
)
}
}
composable(Screen.Profile.route) {
if (!isAuthenticated) {
LaunchedEffect(Unit) {
navController.navigate(Screen.Home.route) {
popUpTo(Screen.Home.route) { inclusive = true }
}
}
} else {
ProfileScreen(
username = currentUser?.username ?: "",
burritoCount = currentUser?.burritoConsiderations ?: 0
)
}
}
}
}
---
app/src/main/java/com/example/posthog/ui/components/AppHeader.kt
package com.example.posthog.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.posthog.ui.theme.DarkHeader
import com.example.posthog.ui.theme.ErrorRed
import com.example.posthog.ui.theme.White
@Composable
fun AppHeader(
isAuthenticated: Boolean,
username: String?,
currentRoute: String?,
onNavigate: (String) -> Unit,
onLogout: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(DarkHeader)
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// App title
Text(
text = "Burrito App",
color = White,
fontSize = 18.sp
)
// User section (right side)
if (isAuthenticated && username != null) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = username,
color = White,
fontSize = 14.sp
)
Button(
onClick = onLogout,
colors = ButtonDefaults.buttonColors(
containerColor = ErrorRed
),
shape = RoundedCornerShape(4.dp),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp)
) {
Text(
text = "Logout",
color = White,
fontSize = 14.sp
)
}
}
}
}
}
}
---
app/src/main/java/com/example/posthog/ui/components/BottomNavBar.kt
package com.example.posthog.ui.components
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.posthog.navigation.Screen
import com.example.posthog.ui.theme.PrimaryBlue
import com.example.posthog.ui.theme.TextGray
import com.example.posthog.ui.theme.White
sealed class BottomNavItem(
val route: String,
val label: String,
val selectedIcon: ImageVector?,
val unselectedIcon: ImageVector?
) {
object Home : BottomNavItem(
route = Screen.Home.route,
label = "Home",
selectedIcon = Icons.Filled.Home,
unselectedIcon = Icons.Outlined.Home
)
object Burrito : BottomNavItem(
route = Screen.Burrito.route,
label = "Burrito",
selectedIcon = null, // We'll use a custom icon or emoji
unselectedIcon = null
)
object Profile : BottomNavItem(
route = Screen.Profile.route,
label = "Profile",
selectedIcon = Icons.Filled.Person,
unselectedIcon = Icons.Outlined.Person
)
}
@Composable
fun BottomNavBar(
isAuthenticated: Boolean,
currentRoute: String?,
onNavigate: (String) -> Unit
) {
val items = if (isAuthenticated) {
listOf(BottomNavItem.Home, BottomNavItem.Burrito, BottomNavItem.Profile)
} else {
listOf(BottomNavItem.Home)
}
NavigationBar(
containerColor = White
) {
items.forEach { item ->
val selected = currentRoute == item.route
NavigationBarItem(
selected = selected,
onClick = { onNavigate(item.route) },
icon = {
if (item.selectedIcon != null && item.unselectedIcon != null) {
Icon(
imageVector = if (selected) item.selectedIcon else item.unselectedIcon,
contentDescription = item.label,
modifier = Modifier.size(24.dp)
)
} else {
// For Burrito, use text emoji as icon
Text(
text = "🌯",
fontSize = 24.sp
)
}
},
label = {
Text(
text = item.label,
fontSize = 12.sp
)
},
colors = NavigationBarItemDefaults.colors(
selectedIconColor = PrimaryBlue,
selectedTextColor = PrimaryBlue,
unselectedIconColor = TextGray,
unselectedTextColor = TextGray,
indicatorColor = PrimaryBlue.copy(alpha = 0.1f)
)
)
}
}
}
---
app/src/main/java/com/example/posthog/ui/components/StatsCard.kt
package com.example.posthog.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.posthog.ui.theme.LightGray
import com.example.posthog.ui.theme.TextDark
import com.example.posthog.ui.theme.TextGray
@Composable
fun StatsCard(
title: String,
value: String,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.background(
color = LightGray,
shape = RoundedCornerShape(4.dp)
)
.padding(16.dp)
) {
Text(
text = title,
color = TextGray,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = value,
color = TextDark,
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
}
}
---
app/src/main/java/com/example/posthog/ui/screens/BurritoScreen.kt
package com.example.posthog.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.posthog.ui.components.StatsCard
import com.example.posthog.ui.theme.BackgroundGray
import com.example.posthog.ui.theme.SuccessGreen
import com.example.posthog.ui.theme.TextDark
import com.example.posthog.ui.theme.TextGray
import com.example.posthog.ui.theme.White
import kotlinx.coroutines.delay
@Composable
fun BurritoScreen(
burritoCount: Int,
onConsiderBurrito: () -> Unit
) {
var showSuccess by remember { mutableStateOf(false) }
LaunchedEffect(showSuccess) {
if (showSuccess) {
delay(2000)
showSuccess = false
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(BackgroundGray)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.widthIn(max = 600.dp)
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Main content card
Column(
modifier = Modifier
.fillMaxWidth()
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(8.dp),
ambientColor = TextDark.copy(alpha = 0.1f),
spotColor = TextDark.copy(alpha = 0.1f)
)
.background(
color = White,
shape = RoundedCornerShape(8.dp)
)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Burrito consideration zone",
fontSize = 24.sp,
fontWeight = FontWeight.SemiBold,
color = TextDark,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Take a moment to truly consider the burrito.",
fontSize = 16.sp,
color = TextGray,
textAlign = TextAlign.Center,
lineHeight = 26.sp
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
onConsiderBurrito()
showSuccess = true
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = SuccessGreen
),
shape = RoundedCornerShape(4.dp)
) {
Text(
text = "Consider the Burrito",
fontSize = 18.sp,
color = White
)
}
if (showSuccess) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "You have considered the burrito. Well done!",
color = SuccessGreen,
fontSize = 16.sp,
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Consideration stats",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = TextDark,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
StatsCard(
title = "Total Burrito Considerations",
value = burritoCount.toString()
)
}
}
}
}
---
app/src/main/java/com/example/posthog/ui/screens/HomeScreen.kt
package com.example.posthog.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.posthog.ui.theme.BackgroundGray
import com.example.posthog.ui.theme.BorderGray
import com.example.posthog.ui.theme.PrimaryBlue
import com.example.posthog.ui.theme.TextDark
import com.example.posthog.ui.theme.TextGray
import com.example.posthog.ui.theme.White
@Composable
fun HomeScreen(
isAuthenticated: Boolean,
username: String?,
onLogin: (String) -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(BackgroundGray)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
contentAlignment = if (isAuthenticated) Alignment.TopCenter else Alignment.Center
) {
Column(
modifier = Modifier
.widthIn(max = 600.dp)
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isAuthenticated && username != null) {
LoggedInContent(username = username)
} else {
LoginForm(onLogin = onLogin)
}
}
}
}
@Composable
private fun LoggedInContent(username: String) {
ContentCard {
Text(
text = "Welcome back, $username!",
fontSize = 24.sp,
fontWeight = FontWeight.SemiBold,
color = TextDark
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Ready to consider some burritos?",
fontSize = 16.sp,
color = TextGray,
lineHeight = 26.sp
)
}
}
@Composable
private fun LoginForm(onLogin: (String) -> Unit) {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
ContentCard {
Text(
text = "Welcome to Burrito Consideration App",
fontSize = 24.sp,
fontWeight = FontWeight.SemiBold,
color = TextDark,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
// Username field
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Username",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = TextDark,
modifier = Modifier.padding(bottom = 8.dp)
)
OutlinedTextField(
value = username,
onValueChange = { username = it },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(4.dp),
colors = OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = BorderGray,
focusedBorderColor = PrimaryBlue,
unfocusedContainerColor = White,
focusedContainerColor = White
)
)
}
Spacer(modifier = Modifier.height(16.dp))
// Password field
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Password",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = TextDark,
modifier = Modifier.padding(bottom = 8.dp)
)
OutlinedTextField(
value = password,
onValueChange = { password = it },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
shape = RoundedCornerShape(4.dp),
colors = OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = BorderGray,
focusedBorderColor = PrimaryBlue,
unfocusedContainerColor = White,
focusedContainerColor = White
)
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
if (username.isNotBlank()) {
onLogin(username)
}
},
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue
),
shape = RoundedCornerShape(4.dp)
) {
Text(
text = "Sign In",
fontSize = 16.sp,
color = White
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Note: This is a demo app. Enter any username to sign in.",
fontSize = 14.sp,
color = TextGray,
textAlign = TextAlign.Center,
lineHeight = 21.sp
)
}
}
@Composable
private fun ContentCard(
content: @Composable () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(8.dp),
ambientColor = TextDark.copy(alpha = 0.1f),
spotColor = TextDark.copy(alpha = 0.1f)
)
.background(
color = White,
shape = RoundedCornerShape(8.dp)
)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
content()
}
}
---
app/src/main/java/com/example/posthog/ui/screens/ProfileScreen.kt
package com.example.posthog.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.posthog.ui.components.StatsCard
import com.example.posthog.ui.theme.BackgroundGray
import com.example.posthog.ui.theme.TextDark
import com.example.posthog.ui.theme.TextGray
import com.example.posthog.ui.theme.White
@Composable
fun ProfileScreen(
username: String,
burritoCount: Int
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(BackgroundGray)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.widthIn(max = 600.dp)
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Main content card
Column(
modifier = Modifier
.fillMaxWidth()
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(8.dp),
ambientColor = TextDark.copy(alpha = 0.1f),
spotColor = TextDark.copy(alpha = 0.1f)
)
.background(
color = White,
shape = RoundedCornerShape(8.dp)
)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "User Profile",
fontSize = 24.sp,
fontWeight = FontWeight.SemiBold,
color = TextDark
)
Spacer(modifier = Modifier.height(24.dp))
// Your Information section
Text(
text = "Your Information",
fontSize = 24.sp,
fontWeight = FontWeight.SemiBold,
color = TextDark,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Username display
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Username",
fontSize = 14.sp,
color = TextGray
)
Text(
text = username,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = TextDark
)
}
Spacer(modifier = Modifier.height(24.dp))
// Stats card
StatsCard(
title = "Total Burrito Considerations",
value = burritoCount.toString()
)
Spacer(modifier = Modifier.height(24.dp))
// Your Burrito Journey section
Text(
text = "Your Burrito Journey",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = TextDark,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = getJourneyMessage(burritoCount),
fontSize = 16.sp,
color = TextGray,
textAlign = TextAlign.Start,
lineHeight = 26.sp,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
private fun getJourneyMessage(count: Int): String = when {
count == 0 -> "You haven't considered any burritos yet. Start your journey!"
count == 1 -> "You've considered the burrito potential once. The journey begins!"
count in 2..4 -> "You're getting the hang of burrito consideration!"
count in 5..9 -> "You're becoming a burrito consideration expert!"
else -> "You are a true burrito consideration master!"
}
---
app/src/main/java/com/example/posthog/ui/theme/Color.kt
package com.example.posthog.ui.theme
import androidx.compose.ui.graphics.Color
val PrimaryBlue = Color(0xFF0070F3)
val PrimaryBlueHover = Color(0xFF0051CC)
val SuccessGreen = Color(0xFF28A745)
val SuccessGreenHover = Color(0xFF218838)
val ErrorRed = Color(0xFFDC3545)
val ErrorRedHover = Color(0xFFC82333)
val DarkHeader = Color(0xFF333333)
val DarkHeaderHover = Color(0xFF555555)
val LightGray = Color(0xFFF8F9FA)
val BorderGray = Color(0xFFDDDDDD)
val TextGray = Color(0xFF666666)
val BackgroundGray = Color(0xFFF5F5F5)
val TextDark = Color(0xFF333333)
val White = Color(0xFFFFFFFF)
---
app/src/main/java/com/example/posthog/ui/theme/Theme.kt
package com.example.posthog.ui.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
private val LightColorScheme = lightColorScheme(
primary = PrimaryBlue,
secondary = SuccessGreen,
tertiary = DarkHeader,
background = BackgroundGray,
surface = White,
onPrimary = White,
onSecondary = White,
onTertiary = White,
onBackground = TextDark,
onSurface = TextDark
)
@Composable
fun PostHogTheme(
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = LightColorScheme,
typography = Typography,
content = content
)
}---
app/src/main/java/com/example/posthog/ui/theme/Type.kt
package com.example.posthog.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Typography based on design specification
// Uses system font stack (FontFamily.Default maps to Roboto on Android)
val Typography = Typography(
// H1 - Page titles (32sp)
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
// H2 - Section titles (24sp)
displayMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
// H3 - Subsection titles (20sp)
displaySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
lineHeight = 26.sp,
letterSpacing = 0.sp
),
// Body text (16sp with 1.6 line height = 25.6sp)
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 26.sp,
letterSpacing = 0.sp
),
// Small/Note text (14sp)
bodySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 21.sp,
letterSpacing = 0.sp
),
// Labels (16sp, medium weight)
labelLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.sp
),
// Button text - Burrito button (18sp)
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 18.sp,
lineHeight = 24.sp,
letterSpacing = 0.sp
),
// Button text - Primary/Logout (16sp/14sp)
titleMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.sp
),
titleSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.sp
)
)---
app/src/main/java/com/example/posthog/viewmodel/AuthViewModel.kt
package com.example.posthog.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.example.posthog.data.User
import com.example.posthog.data.UserRepository
import com.posthog.PostHog
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class AuthViewModel(application: Application) : AndroidViewModel(application) {
private val repository = UserRepository(application)
private val _currentUser = MutableStateFlow<User?>(null)
val currentUser: StateFlow<User?> = _currentUser.asStateFlow()
private val _isAuthenticated = MutableStateFlow(false)
val isAuthenticated: StateFlow<Boolean> = _isAuthenticated.asStateFlow()
init {
loadCurrentUser()
}
private fun loadCurrentUser() {
viewModelScope.launch {
val user = repository.getCurrentUser()
_currentUser.value = user
_isAuthenticated.value = user != null
}
}
fun login(username: String) {
viewModelScope.launch {
val existingUser = repository.getUser(username)
val user = existingUser ?: User(username = username, burritoConsiderations = 0)
repository.saveUser(user)
_currentUser.value = user
_isAuthenticated.value = true
PostHog.identify(username)
PostHog.capture(event = "user_logged_in")
}
}
fun logout() {
viewModelScope.launch {
PostHog.capture("user_logged_out")
PostHog.reset()
repository.clearCurrentUser()
_currentUser.value = null
_isAuthenticated.value = false
}
}
fun incrementBurritoCount() {
viewModelScope.launch {
val user = _currentUser.value ?: return@launch
val updatedUser = user.copy(burritoConsiderations = user.burritoConsiderations + 1)
repository.saveUser(updatedUser)
_currentUser.value = updatedUser
PostHog.capture(
event = "burrito_considered",
properties = mapOf(
"total_considerations" to updatedUser.burritoConsiderations,
"username" to updatedUser.username
)
)
}
}
}
---
PostHog astro-static Example Project
Repository: https://github.com/PostHog/context-mill Path: basics/astro-static
---
README.md
PostHog Astro Static Example
This is an Astro static site (SSG) example demonstrating PostHog integration with product analytics, session replay, and error tracking.
It uses the PostHog web snippet directly and shows how to:
- Initialize PostHog in a static Astro site using a reusable component
- Identify users after login
- Track custom events from pages
- Capture errors via
posthog.captureException() - Reset PostHog state on logout
Features
- Product analytics: Track login and burrito consideration events
- Session replay: Enabled via PostHog snippet configuration
- Error tracking: Manual error capture sent to PostHog
- Simple auth flow: Demo login using localStorage
Getting started
1. Install dependencies
npm install
# or
pnpm install2. Configure environment variables
Create a .env file in the project root:
PUBLIC_POSTHOG_PROJECT_TOKEN=your_posthog_project_token
PUBLIC_POSTHOG_HOST=https://us.i.posthog.comGet your PostHog project token from your project settings in PostHog.
3. Run the development server
npm run dev
# or
pnpm devOpen http://localhost:4321 in your browser.
Project structure
src/
components/
posthog.astro # PostHog snippet with is:inline directive
Header.astro # Navigation + logout, calls posthog.reset()
layouts/
PostHogLayout.astro # Root layout that includes PostHog + Header
lib/
auth.ts # Auth utilities (localStorage-based)
pages/
index.astro # Login form, identifies user + captures 'user_logged_in'
burrito.astro # Burrito consideration demo, captures 'burrito_considered'
profile.astro # Profile + error tracking demo
styles/
global.css # Global stylesKey integration points
PostHog initialization (src/components/posthog.astro)
The PostHog snippet is included as an inline script to prevent Astro from processing it:
<script is:inline>
!function(t,e){...}(document,window.posthog||[]);
posthog.init('<ph_project_token>', {
api_host: 'https://us.i.posthog.com',
defaults: '2026-01-30'
})
</script>The is:inline directive is required to prevent TypeScript errors about window.posthog.
User identification (src/pages/index.astro)
After a successful "login", the app identifies the user and captures a login event:
window.posthog?.identify(username);
window.posthog?.capture("user_logged_in");Identification happens only on login, all further requests will automatically use the same distinct ID.
Event tracking (src/pages/burrito.astro)
The burrito page tracks a custom event when a user "considers" the burrito:
window.posthog?.capture("burrito_considered", {
total_considerations: newCount,
username: currentUser,
});This shows how to attach useful properties to events (e.g. counts, usernames).
Error tracking (src/pages/profile.astro)
The profile page includes a button to trigger a test error:
try {
throw new Error("Test error for PostHog error tracking");
} catch (err) {
window.posthog?.captureException(err);
}Logout and session reset (src/components/Header.astro)
On logout, both the local auth state and PostHog state are cleared:
window.posthog?.capture("user_logged_out");
localStorage.removeItem("currentUser");
window.posthog?.reset();posthog.reset() clears the current distinct ID and session so the next login starts a fresh identity.
Scripts
# Run dev server
npm run dev
# Build for production
npm run build
# Preview production build
npm run previewLearn more
---
.env.example
PUBLIC_POSTHOG_PROJECT_TOKEN=your_posthog_project_token_here
PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
---
astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({});
---
src/components/Header.astro
---
// Header component with navigation and logout functionality
---
<header class="header">
<div class="header-container">
<nav>
<a href="/">Home</a>
<a href="/burrito" class="auth-link" style="display: none;">Burrito Consideration</a>
<a href="/profile" class="auth-link" style="display: none;">Profile</a>
</nav>
<div class="user-section">
<span class="welcome-text" style="display: none;">Welcome, <span class="username"></span>!</span>
<span class="not-logged-in">Not logged in</span>
<button class="btn-logout" style="display: none;">Logout</button>
</div>
</div>
</header>
<script is:inline>
function updateHeader() {
const currentUser = localStorage.getItem('currentUser');
const authLinks = document.querySelectorAll('.auth-link');
const welcomeText = document.querySelector('.welcome-text');
const notLoggedIn = document.querySelector('.not-logged-in');
const logoutBtn = document.querySelector('.btn-logout');
const usernameSpan = document.querySelector('.username');
if (currentUser) {
authLinks.forEach(link => link.style.display = 'inline');
welcomeText.style.display = 'inline';
notLoggedIn.style.display = 'none';
logoutBtn.style.display = 'inline';
usernameSpan.textContent = currentUser;
} else {
authLinks.forEach(link => link.style.display = 'none');
welcomeText.style.display = 'none';
notLoggedIn.style.display = 'inline';
logoutBtn.style.display = 'none';
}
}
function handleLogout() {
const currentUser = localStorage.getItem('currentUser');
if (currentUser) {
window.posthog?.capture('user_logged_out');
}
localStorage.removeItem('currentUser');
localStorage.removeItem('burritoConsiderations');
// IMPORTANT: Reset the PostHog instance to clear the user session
window.posthog?.reset();
window.location.href = '/';
}
document.addEventListener('DOMContentLoaded', () => {
updateHeader();
document.querySelector('.btn-logout')?.addEventListener('click', handleLogout);
});
// Listen for storage changes (login/logout in other tabs)
window.addEventListener('storage', updateHeader);
</script>
<style>
.header {
background-color: #333;
color: white;
padding: 1rem;
}
.header-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.header nav {
display: flex;
gap: 1rem;
}
.header a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.2s;
}
.header a:hover {
background-color: #555;
text-decoration: none;
}
.user-section {
display: flex;
align-items: center;
gap: 1rem;
}
.btn-logout {
background-color: #dc3545;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-logout:hover {
background-color: #c82333;
}
</style>
---
src/components/posthog.astro
---
// PostHog analytics snippet
// Uses is:inline to prevent Astro from processing the script
---
<script is:inline define:vars={{ apiKey: import.meta.env.PUBLIC_POSTHOG_PROJECT_TOKEN, apiHost: import.meta.env.PUBLIC_POSTHOG_HOST }}>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init(apiKey || '', {
api_host: apiHost || 'https://us.i.posthog.com',
defaults: '2026-01-30'
})
</script>
---
src/layouts/PostHogLayout.astro
---
import PostHog from '../components/posthog.astro';
import Header from '../components/Header.astro';
import '../styles/global.css';
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Astro PostHog Integration Example" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
<PostHog />
</head>
<body>
<Header />
<main>
<slot />
</main>
</body>
</html>
---
src/lib/auth.ts
// Client-side auth utilities for localStorage-based authentication
export interface User {
username: string;
burritoConsiderations: number;
}
export function getCurrentUser(): User | null {
if (typeof window === "undefined") return null;
const username = localStorage.getItem("currentUser");
if (!username) return null;
const considerations = parseInt(
localStorage.getItem("burritoConsiderations") || "0",
10,
);
return {
username,
burritoConsiderations: considerations,
};
}
export function login(username: string, password: string): boolean {
if (!username || !password) return false;
localStorage.setItem("currentUser", username);
// Initialize burrito considerations if not set
if (!localStorage.getItem("burritoConsiderations")) {
localStorage.setItem("burritoConsiderations", "0");
}
return true;
}
export function logout(): void {
localStorage.removeItem("currentUser");
localStorage.removeItem("burritoConsiderations");
}
export function incrementBurritoConsiderations(): number {
const current = parseInt(
localStorage.getItem("burritoConsiderations") || "0",
10,
);
const newCount = current + 1;
localStorage.setItem("burritoConsiderations", newCount.toString());
return newCount;
}
---
src/pages/burrito.astro
---
import PostHogLayout from '../layouts/PostHogLayout.astro';
---
<PostHogLayout title="Burrito Consideration - Astro PostHog Example">
<div class="container">
<h1>Burrito consideration zone</h1>
<p>Take a moment to truly consider the potential of burritos.</p>
<div style="text-align: center;">
<button id="consider-btn" class="btn-burrito">
I have considered the burrito potential
</button>
<p id="success-message" class="success" style="display: none;">
Thank you for your consideration! Count: <span id="consideration-count"></span>
</p>
</div>
<div class="stats">
<h3>Consideration stats</h3>
<p>Total considerations: <span id="total-considerations">0</span></p>
</div>
</div>
</PostHogLayout>
<script is:inline>
function checkAuth() {
const currentUser = localStorage.getItem('currentUser');
if (!currentUser) {
window.location.href = '/';
return false;
}
return true;
}
function updateStats() {
const count = localStorage.getItem('burritoConsiderations') || '0';
document.getElementById('total-considerations').textContent = count;
}
function handleConsideration() {
const currentUser = localStorage.getItem('currentUser');
if (!currentUser) return;
// Increment the count
const currentCount = parseInt(localStorage.getItem('burritoConsiderations') || '0', 10);
const newCount = currentCount + 1;
localStorage.setItem('burritoConsiderations', newCount.toString());
// Update the UI
updateStats();
const successMessage = document.getElementById('success-message');
const considerationCount = document.getElementById('consideration-count');
considerationCount.textContent = newCount;
successMessage.style.display = 'block';
// Hide success message after 2 seconds
setTimeout(() => {
successMessage.style.display = 'none';
}, 2000);
// Capture burrito consideration event in PostHog
window.posthog?.capture('burrito_considered', {
total_considerations: newCount,
username: currentUser
});
}
document.addEventListener('DOMContentLoaded', () => {
if (!checkAuth()) return;
updateStats();
document.getElementById('consider-btn')?.addEventListener('click', handleConsideration);
});
</script>
---
src/pages/index.astro
---
import PostHogLayout from '../layouts/PostHogLayout.astro';
---
<PostHogLayout title="Home - Astro PostHog Example">
<div class="container">
<div id="logged-in-view" style="display: none;">
<h1>Welcome back, <span id="welcome-username"></span>!</h1>
<p>You are now logged in. Feel free to explore:</p>
<ul>
<li>Consider the potential of burritos</li>
<li>View your profile and statistics</li>
</ul>
</div>
<div id="logged-out-view">
<h1>Welcome to Burrito Consideration App</h1>
<p>Please sign in to begin your burrito journey</p>
<form id="login-form" class="form">
<div class="form-group">
<label for="username">Username:</label>
<input
type="text"
id="username"
placeholder="Enter any username"
required
/>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input
type="password"
id="password"
placeholder="Enter any password"
required
/>
</div>
<p id="error-message" class="error" style="display: none;"></p>
<button type="submit" class="btn-primary">Sign In</button>
</form>
<p class="note">
Note: This is a demo app. Use any username and password to sign in.
</p>
</div>
</div>
</PostHogLayout>
<script is:inline>
function updateView() {
const currentUser = localStorage.getItem('currentUser');
const loggedInView = document.getElementById('logged-in-view');
const loggedOutView = document.getElementById('logged-out-view');
const welcomeUsername = document.getElementById('welcome-username');
if (currentUser) {
loggedInView.style.display = 'block';
loggedOutView.style.display = 'none';
welcomeUsername.textContent = currentUser;
} else {
loggedInView.style.display = 'none';
loggedOutView.style.display = 'block';
}
}
function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorMessage = document.getElementById('error-message');
if (!username || !password) {
errorMessage.textContent = 'Please provide both username and password';
errorMessage.style.display = 'block';
return;
}
// Client-side only fake auth - store in localStorage
localStorage.setItem('currentUser', username);
if (!localStorage.getItem('burritoConsiderations')) {
localStorage.setItem('burritoConsiderations', '0');
}
// Identify the user in PostHog (once on login is enough)
window.posthog?.identify(username);
window.posthog?.capture('user_logged_in');
// Clear form
document.getElementById('username').value = '';
document.getElementById('password').value = '';
errorMessage.style.display = 'none';
// Update view
updateView();
// Trigger header update
window.dispatchEvent(new Event('storage'));
}
document.addEventListener('DOMContentLoaded', () => {
updateView();
document.getElementById('login-form')?.addEventListener('submit', handleLogin);
});
// Listen for storage changes
window.addEventListener('storage', updateView);
</script>
---
src/pages/profile.astro
---
import PostHogLayout from '../layouts/PostHogLayout.astro';
---
<PostHogLayout title="Profile - Astro PostHog Example">
<div class="container">
<h1>User Profile</h1>
<div class="stats">
<h2>Your Information</h2>
<p><strong>Username:</strong> <span id="profile-username"></span></p>
<p><strong>Burrito Considerations:</strong> <span id="profile-considerations">0</span></p>
</div>
<div style="margin-top: 2rem;">
<h3>Your Burrito Journey</h3>
<p id="journey-message"></p>
</div>
<div style="margin-top: 2rem;">
<h3>Error Tracking Demo</h3>
<p>Click the button below to trigger a test error and send it to PostHog:</p>
<button id="error-btn" class="btn-error">
Trigger Test Error
</button>
<p id="error-feedback" class="success" style="display: none;">
Error captured and sent to PostHog!
</p>
</div>
</div>
</PostHogLayout>
<script is:inline>
function checkAuth() {
const currentUser = localStorage.getItem('currentUser');
if (!currentUser) {
window.location.href = '/';
return false;
}
return true;
}
function updateProfile() {
const username = localStorage.getItem('currentUser') || '';
const considerations = parseInt(localStorage.getItem('burritoConsiderations') || '0', 10);
document.getElementById('profile-username').textContent = username;
document.getElementById('profile-considerations').textContent = considerations;
// Update journey message based on consideration count
const journeyMessage = document.getElementById('journey-message');
if (considerations === 0) {
journeyMessage.textContent = "You haven't considered any burritos yet. Visit the Burrito Consideration page to start!";
} else if (considerations === 1) {
journeyMessage.textContent = "You've considered the burrito potential once. Keep going!";
} else if (considerations < 5) {
journeyMessage.textContent = "You're getting the hang of burrito consideration!";
} else if (considerations < 10) {
journeyMessage.textContent = "You're becoming a burrito consideration expert!";
} else {
journeyMessage.textContent = "You are a true burrito consideration master!";
}
}
function triggerTestError() {
try {
throw new Error('Test error for PostHog error tracking');
} catch (err) {
// Capture the error in PostHog
window.posthog?.captureException(err);
console.error('Captured error:', err);
// Show feedback to user
const feedback = document.getElementById('error-feedback');
feedback.style.display = 'block';
setTimeout(() => {
feedback.style.display = 'none';
}, 3000);
}
}
document.addEventListener('DOMContentLoaded', () => {
if (!checkAuth()) return;
updateProfile();
document.getElementById('error-btn')?.addEventListener('click', triggerTestError);
});
</script>
---