PostHog enables you to analyze data in real-time, as events come in. Make full use of this power by ingesting live data with our analytics integrations: client libraries, server libraries, as well as third-party platforms.
The purpose of this guide is to help you understand some key concepts with a goal of ingesting live data into PostHog. For simplicity, we'll focus on client libraries as a means of data ingestion.
The guide covers the following:
- Installing and initializing a PostHog library
- How autocapture works with the JavaScript library
- How to capture user events with PostHog
- How to identify and associate users with events
If you prefer to learn by doing, you can get started on the web with the JavaScript snippet.
Note that some events with a never-before-seen distinct ID are deliberately delayed by around a minute. More on that in the "Event ingestion nuances" section.
Install a library
Install the library for the platform you are building your application for.
You can either load the snippet as a script in your HTML:
JavaScript snippet Recommended
This is the simplest way to get PostHog up and running on your website, and only takes a few minutes to set-up.
Add to your website & app
Paste this snippet within the <head>
tags of your website - ideally just inside the closing </head>
tag - on all pages that you wish to track.
<script>!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.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".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_api_key>', {api_host: '<ph_instance_address>'})</script>
Be sure to replace <ph_project_api_key>
and <ph_instance_address>
with your project's values. (You can find the snippet pre-filled with this data in the PostHog app under Project / Settings. (Quick links if you use PostHog Cloud US or PostHog Cloud EU)
What this code does
After adding the snippet to your website, it will automatically start to:
- Capture
$pageview
events when a user visits a page - Track when users click on links or buttons
- Record videos of user sessions that you can play back (if you've enabled the Session recordings feature, which we highly recommend!)
Tips
- Add the snippet on your (marketing) website and your application using the same PostHog project. That means you'll be able to follow a user from the moment they come onto your website, all the way through sign up and their actual usage of your product.
- Track users across multiple domains by including the same project snippet on all sites you wish to track.
- Disable tracking sensitive information by adding the
ph-no-capture
class to elements you don't want to track. (We do this automatically for common fields like passwords and credit cards.)
Next steps
- Add permitted domains in your PostHog project settings so you can create actions using the PostHog toolbar directly on your site - a convenient low-code option.
- Send custom events from your codebase for things like signups, purchases, and more KPIs.
The dedicated JavaScript library page has more details about everything you can do with posthog-js
.
Place the snippet in the <head>
tags of your website, ideally just above the closing </head>
tag. You will need to do this for all pages that you wish to track.
Or you can include it using npm, by doing either:
yarn add posthog-js
And then include it in your files:
import posthog from 'posthog-js'posthog.init('<ph_project_api_key>', { api_host: '<ph_instance_address>' })
If you don't want to send a bunch of test data while you're developing, you could do the following:
if (!window.location.host.includes('127.0.0.1') && !window.location.host.includes('localhost')) {posthog.init('<ph_project_api_key>', { api_host: '<ph_instance_address>' })}
If you're using React or Next.js, checkout our React SDK or Next.js integration.
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
module to your build.gradle
:
dependencies {implementation 'com.posthog.android:posthog:1.+'}
The best place to initialize the client is in your Application
subclass.
public class SampleApp extends Application {private static final String POSTHOG_API_KEY = "<ph_project_api_key>";private static final String POSTHOG_HOST = "<ph_instance_address>";@Overridepublic void onCreate() {// Create a PostHog client with the given context, API key and host.PostHog posthog = new PostHog.Builder(this, POSTHOG_API_KEY, POSTHOG_HOST).captureApplicationLifecycleEvents() // Record certain application events automatically!.recordScreenViews() // Record screen views automatically!.build();// Set the initialized instance as a globally accessible instance.PostHog.setSingletonInstance(posthog);// Now anytime you call PostHog.with, the custom instance will be returned.PostHog posthog = PostHog.with(this);}}
PostHog is available through CocoaPods and Carthage or you can add it as a Swift Package Manager based dependency.
CocoaPods
pod "PostHog", "~> 1.1"
Carthage
github "posthog/posthog-ios"
Swift Package Manager
Add PostHog as a dependency in your Xcode project "Package Dependencies" and select the project target for your app, as appropriate.
For a Swift Package Manager based project, add PostHog as a dependency in your "Package.swift" file's Package dependencies section:
dependencies: [.package(url: "https://github.com/PostHog/posthog-ios.git", from: "2.0.0")],
and then as a dependency for the Package target utilizing PostHog:
.target(name: "myApp",dependencies: [.product(name: "PostHog", package: "posthog-ios")]),
With Objective-C
#import <PostHog/PHGPostHog.h>#import <PostHog/PHGPostHogConfiguration.h>// on posthog.comPHGPostHogConfiguration *configuration = [PHGPostHogConfiguration configurationWithApiKey:@"YOUR_API_KEY"];// self-hostedPHGPostHogConfiguration *configuration = [PHGPostHogConfiguration configurationWithApiKey:@"YOUR_API_KEY"host:@"https://app.posthog.com"];configuration.captureApplicationLifecycleEvents = YES; // Record certain application events automatically!configuration.recordScreenViews = YES; // Record screen views automatically![PHGPostHog setupWithConfiguration:configuration];
With Swift
import PostHog// `host` is optional if you use PostHog Cloud (app.posthog.com)let configuration = PHGPostHogConfiguration(apiKey: "<ph_project_api_key>", host: "<ph_instance_address>")configuration.captureApplicationLifecycleEvents = true; // Record certain application events automatically!configuration.recordScreenViews = true; // Record screen views automatically!PHGPostHog.setup(with: configuration)let posthog = PHGPostHog.shared()
PostHog is available for install via Pub.
Setup your Android, iOS, and/or web sources as described at PostHog.com and generate your API keys.
Set your PostHog API key and change the automatic event tracking (only for Android and iOS) on if you wish the library to take care of it for you.
Remember that the application lifecycle events won't have any special context set for you by the time it is initialized. If you are using a self-hosted instance of PostHog you will need to have the public hostname or IP for your instance as well.
Android
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.posthog.posthog_flutter_example"><application><activity>[...]</activity><meta-data android:name="com.posthog.posthog.API_KEY" android:value="<ph_project_api_key>" /><meta-data android:name="com.posthog.posthog.POSTHOG_HOST" android:value="<ph_instance_address>" /><meta-data android:name="com.posthog.posthog.TRACK_APPLICATION_LIFECYCLE_EVENTS" android:value="false" /><meta-data android:name="com.posthog.posthog.DEBUG" android:value="false" /></application></manifest>
iOS
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict>[...]<key>com.posthog.posthog.API_KEY</key><string><ph_project_api_key></string><key>com.posthog.posthog.POSTHOG_HOST</key><string><ph_instance_address></string><key>com.posthog.posthog.TRACK_APPLICATION_LIFECYCLE_EVENTS</key><false/><false/>[...]</dict></plist>
Web
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>example</title></head><body><script>!function () {var analytics = window.analytics = window.analytics || []; if (!analytics.initialize) if (analytics.invoked) window.console && console.error && console.error("PostHog snippet included twice."); else {analytics.invoked = !0; analytics.methods = ["captureSubmit", "captureClick", "captureLink", "captureForm", "pageview", "identify", "reset", "group", "capture", "ready", "alias", "debug", "page", "once", "off", "on"]; analytics.factory = function (t) { return function () { var e = Array.prototype.slice.call(arguments); e.unshift(t); analytics.push(e); return analytics } }; for (var t = 0; t < analytics.methods.length; t++) { var e = analytics.methods[t]; analytics[e] = analytics.factory(e) } analytics.load = function (t, e) { var n = document.createElement("script"); n.type = "text/javascript"; n.async = !0; n.src = "https://cdn.posthog.com/analytics.js/v1/" + t + "/analytics.min.js"; var a = document.getElementsByTagName("script")[0]; a.parentNode.insertBefore(n, a); analytics._loadOptions = e }; analytics.SNIPPET_VERSION = "4.1.0";analytics.load("YOUR_API_KEY_GOES_HERE");analytics.page();}}();</script><script src="main.dart.js" type="application/javascript"></script></body></html>
For more information please check: https://posthog.com/docs/integrate/client/js
In your React Native or Expo project add the posthog-react-native
package to your project as well as the required peer dependencies.
Expo Apps
expo install posthog-react-native expo-file-system expo-application expo-device expo-localization
React Native Apps
yarn add posthog-react-native @react-native-async-storage/async-storage react-native-device-info# ornpm i -s posthog-react-native @react-native-async-storage/async-storage react-native-device-info
With the PosthogProvider
The recommended flow for setting up PostHog React Native is to use the PostHogProvider
which utilises the Context API to pass the PostHog Client around as well as enabling autocapture and ensuring that the queue is flushed at the right time:
// App.(js|ts)import { usePostHog, PostHogProvider } from 'posthog-react-native'...export function MyApp() {return (<PostHogProvider apiKey="<ph_project_api_key>" options={{// (Optional) PostHog API host (https://app.posthog.com by default)host: '<ph_instance_address>',}}><MyComponent /></PostHogProvider>)}// Now you can simply access posthog elsewhere in the app like soconst MyComponent = () => {const posthog = usePostHog()useEffect(() => {posthog.capture("MyComponent loaded", { foo: "bar" })}, [])}
Without the PosthogProvider
Due to the Async nature of React Native, PostHog needs to be initialised asynchronously in order for the persisted state to be loaded properly. The PosthogProvider
takes care of this under-the-hood but you can alternatively create the instance yourself like so:
// posthog.tsimport PostHog from 'posthog-react-native'export let posthog: PostHog | undefined = undefinedexport const posthogAsync: Promise<PostHog> = PostHog.initAsync('<ph_project_api_key>', {// PostHog API host (https://app.posthog.com by default)host: '<ph_instance_address>'})posthogAsync.then(client => {posthog = client})// app.tsimport { posthog, posthogAsync} from './posthog'export function MyApp1() {useEffect(async () => {// Use posthog optionally with the possibility that it may still be loadingposthog?.capture('MyApp1 loaded')// OR use posthog via the promise(await posthogAsync).capture('MyApp1 loaded')}, [])return <View>Your app code</View>}// You can even use this instance with the PostHogProviderexport function MyApp2() {return <PostHogProvider client={posthogAsync}>{/* Your app code */}</PostHogProvider>}
Configuration options
For most people, the default configuration options will work great but if you need to you can further customise how PostHog will work.
export const posthog = await PostHog.initAsync("<ph_project_api_key>", {// PostHog API hosthost?: string = "https://app.posthog.com",// The number of events to queue before sending to PostHog (flushing)flushAt?: number = 20,// The interval in milliseconds between periodic flushesflushInterval?: number = 10000// If set to false, tracking will be disabled until `optIn` is calledenable?: boolean = true,// Whether to track that `getFeatureFlag` was called (used by Expriements)sendFeatureFlagEvent?: boolean = true,// Whether to load feature flags when initialised or notpreloadFeatureFlags?: boolean = true,// How many times we will retry HTTP requestsfetchRetryCount?: number = 3,// The delay between HTTP request retriesfetchRetryDelay?: number = 3000,// For Session Analysis how long before we expire a sessionsessionExpirationTimeSeconds?: number = 1800 // 30 mins// Whether to post events to PostHog in JSON or compressed formatcaptureMode?: 'json' | 'form'})
Use autocapture
Autocapture is presently only available in the web browser using the JavaScript library. For non-browser platforms and for situations where you wish to manually capture events, see the capture user events section.
When you call posthog.init
the PostHog JS library begins automatically capturing user events:
- pageviews, including the URL
- autocaptured events, such as any click, change of input, or submission associated with
a
,button
,form
,input
,select
,textarea
, andlabel
tags
Autocapture allows you to track the majority of general events on your website right out of the gate
PostHog puts a great amount of effort into making sure it doesn't capture any sensitive data from your website. If there are other elements you don't want to be captured, you can add the ph-no-capture
class name.
<button class='ph-no-capture'>Sensitive information here</button>
Capture user events
This allows you to send more context than the default event info that PostHog captures whenever a user does something. In that case, you can send an event with any metadata you may wish to add.
posthog.capture('[event-name]', { property1: 'value', property2: 'another value' })
A capture
call requires:
event
to specify the event name- We recommend naming events with "[noun][verb]", such as
movie played
ormovie updated
, in order to easily identify what your events mean later on (we know this from experience).
- We recommend naming events with "[noun][verb]", such as
Optionally you can submit:
properties
, which is an object with any information you'd like to add
Identify users
To make sure you understand which user is performing actions within your app, you can identify users at any point. From the moment you make this call, all events will be identified with that distinct id.
The ID can by anything, but is usually the unique ID that you identify users by in the database.
Normally, you would put this below posthog.init
if you have the information there.
If a user was previously anonymous (because they hadn't signed up or logged in yet), we'll automatically alias their anonymous ID with their new unique ID. That means all their events from before and after they signed up will be shown under the same user.
posthog.identify('[user unique id]', // distinct_id, required{ userProperty: 'value1' }, // $set, optional{ anotherUserProperty: 'value2' } // $set_once, optional);
You can also pass two more arguments to posthog.identify
. Both take objects consisting of as many properties as you want to be set on that user's profile. The difference is that the second argument will use the $set
mechanism, whereas the third argument will use $set_once
.
You can read more about the difference between this in the setting properties section.
Warning! You can't call identify straight after an `.init` (as `.init` sends a pageview event, probably with the user's anonymous ID).
To address the issue described above, you can call .init
passing a loaded
callback function, inside which you can then call identify, like so:
posthog.init('<ph_project_api_key>', {api_host: '<ph_instance_address>',loaded: function(posthog) { posthog.identify('[user unique id]'); }});
When you start tracking events with PostHog, each user gets an anonymous ID that is used to identify them in the system.
In order to link this anonymous user with someone from your database, use the identify
call.
Identify lets you add metadata to your users so you can easily identify who they are in PostHog, as well as do things like segment users by these properties.
An identify call requires:
distinctId
which uniquely identifies your user in your databaseuserProperties
with a dictionary of key:value pairs
PostHog.with(this).identify(distinctID, new Properties().putValue("name", "My Name").putValue("email", "user@posthog.com"));
The most obvious place to make this call is whenever a user signs up, or when they update their information.
When you call identify
, all previously tracked anonymous events will be linked to the user.
When you start tracking events with PostHog, each user gets an anonymous ID that is used to identify them in the system.
In order to link this anonymous user with someone from your database, use the identify
call.
Identify lets you add metadata to your users so you can easily identify who they are in PostHog, as well as do things like segment users by these properties.
An identify call requires:
distinct_id
which uniquely identifies your user in your databaseproperties
with a dictionary of key:value pairs
For example:
// in objective-c[[PHGPostHog sharedPostHog] identify:@"distinct_id_from_your_database"properties:@{ @"name": @"Peter Griffin",@"email": @"peter@familyguy.com" }];
// in swiftposthog.identify("user_id_from_your_database",properties: ["name": "Peter Griffin", "email": "peter@familyguy.com"])
The most obvious place to make this call is whenever a user signs up, or when they update their information.
When you call identify
, all previously tracked anonymous events will be linked to the user.
See the Flutter library docs for more information.
When you start tracking events with PostHog, each user gets an anonymous ID that is used to identify them in the system.
In order to link this anonymous user with someone from your database, use the identify
call.
Identify lets you add metadata on your users so you can more easily identify who they are in PostHog, and even do things like segment users by these properties.
An identify call requires:
distinctId
which uniquely identifies your user in your databaseuserProperties
with a dictionary with key: value pairs
posthog.identify('distinctID', {email: 'user@posthog.com',name: 'My Name'})
The most obvious place to make this call is whenever a user signs up, or when they update their information.
When you call identify
, all previously tracked anonymous events will be linked to the user.
Event ingestion nuances
It's a priority for us that events are fully processed and saved as soon as possible. However, there is a class of events which we deliberately process with a slight delay. Specifically, an event is delayed by around a minute if it fits all of the following three conditions:
- isn't from an anonymous user (anonymous users are recognized by having the
distinct_id
the same as the$device_id
property) - isn't an
$identify
event (e.g. fromposthog.identify()
) - its
distinct_id
cannot be matched to an existing person
This delay mechanism is called the event buffer, and it materially improves handling of an edge case which could otherwise inflate unique user counts.
How does the event buffer help?
Starting with version 1.38.0, PostHog stores the person associated with an event inline with the event record. This greatly improves query performance, but because events are immutable, it also means that persons can't be merged retroactively. See this scenario where that's problematic:
- User visits signup page, in turn frontend captures anonymous
$pageview
for distinct IDXYZ
(anonymous distinct ID = device ID).
This event gets person IDA
. - User click signup button, initiating in a backend request, in turn frontend captures anonymous
$autocapture
(click) for distinct IDXYZ
.
This event gets person IDA
. - Signup request is processed in the backend, in turn backend captures identified signup for distinct ID
alice@example
.com.
OOPS! We haven't seenalice@example.com
before, so this event gets person IDB
. - Signup request finishes successfully, in turn frontend captures identified $identify aliasing distinct ID
XYZ
toalice@example.com
.
This event gets person IDA
.
Here, the event from step 3 got a new person ID B
, impacting unique users counts. If it were delayed just a bit and processed after the event from step 4, all events would get the expected person ID A
. This is exactly what the event buffer achieves.