mirror of
https://github.com/BillyOutlast/posthog.com.git
synced 2026-02-04 03:11:21 +01:00
Nail docs navigation (#5186)
* begin refactoring sidebar * completely remove using posthog section * add icons to product sections * remove integrate section altogether * fix merge isssue * remove left-over conflict marker * slight tweaks to sidebar labels * add placeholder landing pages * create tutorials component for docs landing pages * mockup of product-analytics landing page * break apart existing session recordings page * break up feature flags section * break up experiments section * move library comparison page to reference section * mock up feature flags landing page * temp: comment out wrapPageElement in gatsby-browser * fix build * mockup landing pages * fix image positioning * remove duplicates * add clean script * update docs homepage link grid * move deploy options to important links * Tutorial - How to set up a React app heatmap (#5454) * react heatmap * wider images * fix: various self hosted clean-ups (#5473) * remove references to /signup/self-host/deploy * merge self host instructions into landing page * fix wrong bracket * redirect /contact to /contact-sales * change intro message and update guidance on memory size * remove /docs/self-host/open-source/deployment * remove unused DeploymentOption component * remove trailing whitespace * fetch docs homepage info with query * exclude build pages * finalize docs landing page * update path to search hog image * tweak image width and make all landing pages less wide * update links * mobile style for getting started section * fix dark mode sidebar menu * update tutorials component * update docs submenu links * update data start here link * update quick links on all pages * regen mdx global components * update gatsby-browser/ssr * fix product icons * use product icons * moved data-related docs to new subsection, outside of product analytics * text changes * reworked product analytics index page to match chapters * standardized naming for main product manual pages * add key * linked Sampling page * icon strings * polish product analytics getting started page * dark mode fixes * move graphic to top * line up children with icons * fix icon, experiments labeling * dark mode fix * styling docs → product index pages * added getting started images * getting started image mobile fix * start-here images * remove product analytics url * merged identify users content, redirected old page * moved User properties page into Getting started * added User properties to Getting started index * next steps text * update quick links / add session recording & data * text/image fixes * add product analytics URL * fixing links, moving nav items around * fixed broken links, polished /docs styling * redirected old /docs/integrate page * spacing --------- Co-authored-by: Ian Vanagas <34755028+ivanagas@users.noreply.github.com> Co-authored-by: Eli Kinsey <eli@ekinsey.dev> Co-authored-by: Cory Watilo <cww@watilo.com>
This commit is contained in:
@@ -295,7 +295,7 @@ Both Amplitude and PostHog integrate with a large number of data sources. The ta
|
||||
<tr>
|
||||
<td>Zendesk</td>
|
||||
<td className="text-center"><span className="text-green text-lg">✔</span></td>
|
||||
<td className="text-center"><span className="text-green text-lg">✔</span></td>
|
||||
<td className="text-center"><span className="text-green text-lg">✔</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>API</td>
|
||||
|
||||
@@ -16,6 +16,88 @@ Experimentation helps you test changes to your product. You can test hypotheses
|
||||
- You want to test if showing your website copy in German does better than English in Austria.
|
||||
- You want to test if certain copy does better within a cohort of new users.
|
||||
|
||||
## Creating an experiment
|
||||
|
||||
Creating an experiment starts with a hypothesis you want to test. Let's use an example of whether a visual update to the sign up button will increase clicks for that button.
|
||||
|
||||
After clicking on "new experiment" in the Experiments page, we should see a form like this:
|
||||
|
||||

|
||||
|
||||
#### 1. Experiment name, feature flag, and testing variants
|
||||
|
||||
Let's start in the upper left corner by filling out a name, description, and feature flag for our experiment. Fill in a name for your feature flag - it will be created here by default, linked to the experiment rather than existing as its own entity.
|
||||
|
||||

|
||||
|
||||
We can also include multivariants for our testing changes. For example, instead of running our experiment with just "control" and "test", we can test for the green visual button change or the orange one or our original control.
|
||||
|
||||
#### 2. Participant selection and experiment goal/metric
|
||||
|
||||
Next, we want to select our experiment's universe. Here, we've decided that for our sign up button experiment we want to test our hypothesis on users from Asia.
|
||||
|
||||
We also want to set our experiment's "goal" now for which metric we want to track. Since we're testing whether the number of sign up button clicks increase or not, we'll use the "Trend" goal to measure aggregate values
|
||||
|
||||

|
||||
|
||||
If we want to test a hypothesis for a conversion rate metric, we'll use the "Funnel" goal instead.
|
||||
|
||||
#### 3. Experiment preview and minimal acceptable improvement threshold
|
||||
|
||||
Finally, in our experiment creation form, look to the upper right corner for our experiment's preview!
|
||||
We can see recommendations for run times and other experiment factors.
|
||||
|
||||

|
||||
|
||||
We can also increase or decrease our minimal acceptable improvement depending on how much we care for our test's significance relevant to our other experiment factors.
|
||||
|
||||
Perhaps we don't want to wait 278 days for our experiment to run and we'd prefer less precise results in favor of a shorter run time. We can simply bump the threshold to a higher percentage.
|
||||
|
||||

|
||||
|
||||
Whenever we're ready, hit save on the bottom right corner of the form to create an experiment. Creating an experiment saves it into a draft mode so we'll still have flexibility on making adjustments to it until we're ready to launch it!
|
||||
|
||||
## Running an experiment
|
||||
|
||||
Before you can launch an experiment, you need to implement the code for each feature flag variant. On the draft experiment screen, you'll see a snippet telling you how to query for the variant. It looks something like:
|
||||
|
||||
```js
|
||||
if (posthog.getFeatureFlag('your-chosen-feature-flag-key') === 'control') {
|
||||
// where 'control' is the variant, run your code here
|
||||
}
|
||||
```
|
||||
|
||||
One important gotcha to keep in mind is that feature flag values are loaded asynchronously, so you should use [the onFeatureFlag directive](https://posthog.com/docs/user-guides/feature-flags#onfeatureflags) to check when feature flags are loaded. That is, call the snippet above inside the `onFeatureFlag` callback.
|
||||
|
||||
Once you've written your code, it's a good idea to test that each variant behaves as you'd expect. If you find out your implementation had a bug after you've launched the experiment, you lose days of effort as the experiment results can no longer be trusted.
|
||||
|
||||
To test your experiment, **use the web developer console**. The experiment draft screen has a snippet for testing your code. Once you've pushed your experiment code, open the console on your browser, and paste the snippet in. This will override the feature flag for you, and you should be able to see the changes you've made on screen.
|
||||
|
||||
Once you're satisfied, you're ready to launch your experiment.
|
||||
|
||||
**Note:** The feature flag is activated only when you launch the experiment.
|
||||
|
||||
**Note:** While the toolbar allows you to toggle feature flags on and off, this only works for active feature flags. Thus, it's not always possible to use the toolbar to test experiment feature flag changes.
|
||||
|
||||
While the experiment is running, you can see results on the page. Sometimes, in the beginning of an experiment, results can be skewed to one side. For example, if you launched at midnight then people on the other side of the world are active first, and the first few hours of data you collect is all from them. This can artificially skew results to one variant. While possible, it's important that you don't end your experiments at this stage: the results are probably insignificant. This is known as the peeking problem.
|
||||
|
||||
While peeking at the results in itself is not a problem, making quick decisions based on preliminary results is problematic. For this reason, we display a banner that informs you whether it's safe to end an experiment or not.
|
||||
|
||||
Note that this recommendation doesn't cover all edge cases. It is still possible, when new data comes in, that results flip completely. This is true for any experiments and any significance test in existence today. But, the probability of this happening is very low, which is why most people are comfortable taking this risk. For a 0 risk experiment, you'd have to run it for an infinite amount of time: every new person is new data to be accommodated.
|
||||
|
||||
## Terminating an experiment
|
||||
|
||||
You decide when to terminate an experiment. A banner in your experiment results page will let you know if it's safe to do so, regardless of the original estimated running time. Terminating before significance implies conclusions could be completely wrong. (see [Advanced section below](#advanced-whats-under-the-hood) to understand how we compute significance)
|
||||
|
||||
Once you have decided to terminate an experiment there are a few things to do:
|
||||
|
||||
- [ ] Click on "Terminate the experiment" on the experiment page. This will ensure final results are kept.
|
||||
- [ ] We recommend you roll-out the winning variant to all your users.
|
||||
- [ ] Share results with relevant members of your team.
|
||||
- [ ] After the winning variant is rolled-out, it's good practice to remove the other variants from your code, and make the winning variant part of your core code (i.e. stop checking for the respective feature flag).
|
||||
- [ ] Document conclusions and findings in your PostHog experiment. This will help preserve context for team members who in the future need to understand what happened.
|
||||
- [ ] Archive the experiment. This will disable the feature flag and ensure your code no longer depends on the flag.
|
||||
|
||||
## How does it work?
|
||||
|
||||
With Experimentation, you start with a hypothesis on how to improve your product (e.g. changing the flow, changing copy, changing visual styles, etc.) and a goal (e.g. improve conversion, improve sign ups, etc.). Once you have those you can:
|
||||
@@ -54,191 +136,8 @@ Experimentation uses multivariate Feature Flags under the hood to handle user al
|
||||
|
||||
While Feature Flags can be boolean or multivariate, Experimentation always uses a multivariate approach.
|
||||
|
||||
## How to use Experimentation
|
||||
|
||||
### Creating an experiment
|
||||
|
||||
Creating an experiment starts with a hypothesis you want to test. Let's use an example of whether a visual update to the sign up button will increase clicks for that button.
|
||||
|
||||
After clicking on "new experiment" in the Experiments page, we should see a form like this:
|
||||
|
||||

|
||||
|
||||
#### 1. Experiment name, feature flag, and testing variants
|
||||
|
||||
Let's start in the upper left corner by filling out a name, description, and feature flag for our experiment. Fill in a name for your feature flag - it will be created here by default, linked to the experiment rather than existing as its own entity.
|
||||
|
||||

|
||||
|
||||
We can also include multivariants for our testing changes. For example, instead of running our experiment with just "control" and "test", we can test for the green visual button change or the orange one or our original control.
|
||||
|
||||
#### 2. Participant selection and experiment goal/metric
|
||||
|
||||
Next, we want to select our experiment's universe. Here, we've decided that for our sign up button experiment we want to test our hypothesis on users from Asia.
|
||||
|
||||
We also want to set our experiment's "goal" now for which metric we want to track. Since we're testing whether the number of sign up button clicks increase or not, we'll use the "Trend" goal to measure aggregate values
|
||||
|
||||

|
||||
|
||||
If we want to test a hypothesis for a conversion rate metric, we'll use the "Funnel" goal instead.
|
||||
|
||||
#### 3. Experiment preview and minimal acceptable improvement threshold
|
||||
|
||||
Finally, in our experiment creation form, look to the upper right corner for our experiment's preview!
|
||||
We can see recommendations for run times and other experiment factors.
|
||||
|
||||

|
||||
|
||||
We can also increase or decrease our minimal acceptable improvement depending on how much we care for our test's significance relevant to our other experiment factors.
|
||||
|
||||
Perhaps we don't want to wait 278 days for our experiment to run and we'd prefer less precise results in favor of a shorter run time. We can simply bump the threshold to a higher percentage.
|
||||
|
||||

|
||||
|
||||
Whenever we're ready, hit save on the bottom right corner of the form to create an experiment. Creating an experiment saves it into a draft mode so we'll still have flexibility on making adjustments to it until we're ready to launch it!
|
||||
|
||||
### Running an experiment
|
||||
|
||||
Before you can launch an experiment, you need to implement the code for each feature flag variant. On the draft experiment screen, you'll see a snippet telling you how to query for the variant. It looks something like:
|
||||
|
||||
```js
|
||||
if (posthog.getFeatureFlag('your-chosen-feature-flag-key') === 'control') {
|
||||
// where 'control' is the variant, run your code here
|
||||
}
|
||||
```
|
||||
|
||||
One important gotcha to keep in mind is that feature flag values are loaded asynchronously, so you should use [the onFeatureFlag directive](https://posthog.com/docs/user-guides/feature-flags#onfeatureflags) to check when feature flags are loaded. That is, call the snippet above inside the `onFeatureFlag` callback.
|
||||
|
||||
Once you've written your code, it's a good idea to test that each variant behaves as you'd expect. If you find out your implementation had a bug after you've launched the experiment, you lose days of effort as the experiment results can no longer be trusted.
|
||||
|
||||
To test your experiment, **use the web developer console**. The experiment draft screen has a snippet for testing your code. Once you've pushed your experiment code, open the console on your browser, and paste the snippet in. This will override the feature flag for you, and you should be able to see the changes you've made on screen.
|
||||
|
||||
Once you're satisfied, you're ready to launch your experiment.
|
||||
|
||||
**Note:** The feature flag is activated only when you launch the experiment.
|
||||
|
||||
**Note:** While the toolbar allows you to toggle feature flags on and off, this only works for active feature flags. Thus, it's not always possible to use the toolbar to test experiment feature flag changes.
|
||||
|
||||
While the experiment is running, you can see results on the page. Sometimes, in the beginning of an experiment, results can be skewed to one side. For example, if you launched at midnight then people on the other side of the world are active first, and the first few hours of data you collect is all from them. This can artificially skew results to one variant. While possible, it's important that you don't end your experiments at this stage: the results are probably insignificant. This is known as the peeking problem.
|
||||
|
||||
While peeking at the results in itself is not a problem, making quick decisions based on preliminary results is problematic. For this reason, we display a banner that informs you whether it's safe to end an experiment or not.
|
||||
|
||||
Note that this recommendation doesn't cover all edge cases. It is still possible, when new data comes in, that results flip completely. This is true for any experiments and any significance test in existence today. But, the probability of this happening is very low, which is why most people are comfortable taking this risk. For a 0 risk experiment, you'd have to run it for an infinite amount of time: every new person is new data to be accommodated.
|
||||
|
||||
### Terminating an experiment
|
||||
|
||||
You decide when to terminate an experiment. A banner in your experiment results page will let you know if it's safe to do so, regardless of the original estimated running time. Terminating before significance implies conclusions could be completely wrong. (see [Advanced section below](#advanced-whats-under-the-hood) to understand how we compute significance)
|
||||
|
||||
Once you have decided to terminate an experiment there are a few things to do:
|
||||
|
||||
- [ ] Click on "Terminate the experiment" on the experiment page. This will ensure final results are kept.
|
||||
- [ ] We recommend you roll-out the winning variant to all your users.
|
||||
- [ ] Share results with relevant members of your team.
|
||||
- [ ] After the winning variant is rolled-out, it's good practice to remove the other variants from your code, and make the winning variant part of your core code (i.e. stop checking for the respective feature flag).
|
||||
- [ ] Document conclusions and findings in your PostHog experiment. This will help preserve context for team members who in the future need to understand what happened.
|
||||
- [ ] Archive the experiment. This will disable the feature flag and ensure your code no longer depends on the flag.
|
||||
|
||||
## Advanced: What's under the hood?
|
||||
|
||||
Below are all formulas and calculations we go through when recommending sample sizes and determining significance.
|
||||
|
||||
### How we ensure distribution of people
|
||||
|
||||
For every experiment, we leverage PostHog's multivariate feature flags. There's one control group, and up to three test groups. Based on their distinctID, each user is randomly distributed into one of these groups. This is stable, so the same users, even when they revisit your page, stay in the same group.
|
||||
|
||||
We do this by creating a hash out of the feature flag key and the distinctID. It's worth noting that when you have low data (<1,000 users per variant), the difference in variant exposure can be up to 20%. This means, a test variant could have 800 people only, when control has 1,000.
|
||||
|
||||
All our calculations take this exposure into account.
|
||||
|
||||
### Recommendations for sample size and running time
|
||||
|
||||
When you're creating an experiment, we show you recommended running times and sample sizes based on parameters you've chosen.
|
||||
|
||||
For trend experiments, we use Lehr's equation [as explained here](http://www.columbia.edu/~cjd11/charles_dimaggio/DIRE/styled-4/code-12/#poisson-distributed-or-count-data) to determine sample sizes.
|
||||
|
||||
```
|
||||
exposure = 4 / (sqrt(lambda1) - sqrt(lambda2))^2
|
||||
```
|
||||
|
||||
where lambda1 is the baseline count data we've seen for the past two weeks,
|
||||
and lambda2 is `baseline count + mde*(baseline count)`.
|
||||
|
||||
`mde` is the minimum acceptable improvement you choose in the UI.
|
||||
|
||||
For funnel experiments, we use the general [Sample Size Determination](https://en.wikipedia.org/wiki/Sample_size_determination) formula, with 80% power and 5% significance. The formula then becomes:
|
||||
|
||||
```
|
||||
sample size per variant = 16 * conversion_rate * (1 - conversion_rate) / (mde)^2
|
||||
```
|
||||
|
||||
where `mde` is again the minimum detectable effect chosen in the UI.
|
||||
|
||||
We give these values as an estimate for how long to run the experiment. It's possible to end experiments before you reach the end, if you see an outsized effect.
|
||||
|
||||
Note how the recommended sample size in each case is inversely related to the minimum acceptable improvement. This makes sense, since the smaller the `mde`, the more sensitive your experiment is, and the more data you need to judge significance.
|
||||
|
||||
### Bayesian A/B testing
|
||||
|
||||
We follow a mostly bayesian approach to A/B testing. While running any experiment, we calculate two parameters: (1) Probability of each variant being the best, and (2) whether the results are significant or not.
|
||||
|
||||
Below are calculations for each kind of experiment.
|
||||
|
||||
### Trend experiment calculations
|
||||
|
||||
Trend experiments capture count data. For example, if you want to measure the change in total count of clicks, you'd use this kind of experiment.
|
||||
|
||||
We use Monte Carlo simulations to determine the probability of each variant being the best. Every variant can be simulated as a gamma distribution with shape parameter = trend count, and exposure = relative exposure for this variant.
|
||||
|
||||
Then, for each variant, we can sample from their distributions and get a count value for each of them.
|
||||
|
||||
The probability of a variant being the best is given by:
|
||||
|
||||

|
||||
|
||||
For calculating significance, we currently measure p-values using a poisson means test. [Here's a good primer of the formula](https://www.evanmiller.org/statistical-formulas-for-programmers.html#count_test). Results are significant when the p-value is less than 0.05
|
||||
|
||||
### Trend experiment exposure
|
||||
|
||||
Since count data can be over a total count, vs. the number of unique users, we use a proxy metric to measure exposure: The number of times `$feature_flag_called` event returns `control` or `test` is the respective exposure for the variant. This event is sent automatically when you do: `posthog.getFeatureFlag()`.
|
||||
|
||||
It's possible that a variant showing fewer count data can have higher probability, if its exposure is much smaller as well.
|
||||
|
||||
### Funnel experiment calculations
|
||||
|
||||
Funnel experiments capture conversion rates. For example, if you want to measure the change in conversion rate for buying a subscription to your site, you'd use this kind of experiment.
|
||||
|
||||
We use monte carlo simulations to determine the probability of each variant being the best. Every variant can be simulated as a beta distribution with alpha parameter = number of conversions, and beta parameter = number of failures, for this variant.
|
||||
|
||||
Then, for each variant, we can sample from their distributions and get a conversion rate for each of them.
|
||||
|
||||
The probability of a variant being the best is given by:
|
||||
|
||||

|
||||
|
||||
To calculate significance, we calculate the expected loss, as first mentioned in [VWO's SmartStats whitepaper](https://vwo.com/downloads/VWO_SmartStats_technical_whitepaper.pdf).
|
||||
|
||||
To do this, we again run a monte carlo simulation, and calculate loss as:
|
||||
|
||||

|
||||
|
||||
This represents the expected loss in conversion rate if you chose any other variant. If this is below 1%, we declare results as significant.
|
||||
|
||||
### Important note about significance
|
||||
|
||||
In the early days of an experiment, data can vary wildly, and sometimes one variant can seem overwhelmingly better. In this case, our significance calculations might say that the results are significant, but this shouldn't be the case, since we need more data.
|
||||
|
||||
Thus, before we hit 100 participants for each variant in an experiment, we default to results being not significant. Further, if the probability of the winning variant is less than 90%, we default to results being not significant.
|
||||
|
||||
So, you'll only see the green significance banner when all 3 conditions are met:
|
||||
|
||||
1. Each variant has >100 unique users
|
||||
2. The calculations above declare significance
|
||||
3. The probability of being the best > 90%.
|
||||
|
||||
### Further reading
|
||||
|
||||
Want to know more about what's possible with Experiments in PostHog? Try these tutorials:
|
||||
|
||||
- [How to run Experiments without feature flags](/tutorials/experiments)
|
||||
|
||||
Want more? Check our [full list of PostHog tutorials](https://posthog.com/tutorials).
|
||||
28
contents/docs/experiments/significance.mdx
Normal file
28
contents/docs/experiments/significance.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Statistical significance
|
||||
---
|
||||
|
||||
For your results and conclusions to be valid, any experiment must have a significant exposure. For instance, if you test a product change and only one user sees the change, you can't extrapolate from that single user that the change will be beneficial/detrimental for your entire user base. This is true for any experiment that is a simple randomized controlled experiment (e.g. this is also done when testing new drugs or vaccines).
|
||||
|
||||
Furthermore, even a large sample size (e.g. approx. 10,000 participants) can result in ambiguous results. If, for example, the difference in conversion rate between the variants is less than 1%, it's hard to say whether one variant is truly better than the other. To be significant, there must be enough difference between the conversion rates, given the exposure size.
|
||||
|
||||
PostHog computes this significance for you automatically - we will let you know if your experiment has reached significant results or not. Once your experiment reaches significant results, it's safe to use those results to reach a conclusion and terminate the experiment. You can read more about how we do this in our 'Advanced' section below.
|
||||
|
||||
## Calculating exposure
|
||||
|
||||
Since count data can be over a total count, vs. the number of unique users, we use a proxy metric to measure exposure: The number of times `$feature_flag_called` event returns `control` or `test` is the respective exposure for the variant.
|
||||
This event is sent automatically when you do: `posthog.getFeatureFlag()`.
|
||||
|
||||
It's possible that a variant showing fewer count data can have higher probability, if its exposure is much smaller as well.
|
||||
|
||||
## How we determine significance
|
||||
|
||||
In the early days of an experiment, data can vary wildly, and sometimes one variant can seem overwhelmingly better. In this case, our significance calculations might say that the results are significant, but this shouldn't be the case, since we need more data.
|
||||
|
||||
Thus, before we hit 100 participants for each variant in an experiment, we default to results being not significant. Further, if the probability of the winning variant is less than 90%, we default to results being not significant.
|
||||
|
||||
So, you'll only see the green significance banner when all 3 conditions are met:
|
||||
|
||||
1. Each variant has >100 unique users
|
||||
2. The calculations above declare significance
|
||||
3. The probability of being the best > 90%.
|
||||
105
contents/docs/experiments/under-the-hood.mdx
Normal file
105
contents/docs/experiments/under-the-hood.mdx
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Experiments under the hood
|
||||
---
|
||||
|
||||
Below are all formulas and calculations we go through when recommending sample sizes and determining significance.
|
||||
|
||||
## How we ensure distribution of people
|
||||
|
||||
For every experiment, we leverage PostHog's multivariate feature flags. There's one control group, and up to three test groups. Based on their distinctID, each user is randomly distributed into one of these groups. This is stable, so the same users, even when they revisit your page, stay in the same group.
|
||||
|
||||
We do this by creating a hash out of the feature flag key and the distinct ID.
|
||||
It's worth noting that when you have low data (<1,000 users per variant), the difference in variant exposure can be up to 20%. This means, a test variant could have 800 people only, when control has 1,000.
|
||||
|
||||
All our calculations take this exposure into account.
|
||||
|
||||
## Recommendations for sample size and running time
|
||||
|
||||
When you're creating an experiment, we show you recommended running times and sample sizes based on parameters you've chosen.
|
||||
|
||||
For trend experiments, we use Lehr's equation [as explained here](http://www.columbia.edu/~cjd11/charles_dimaggio/DIRE/styled-4/code-12/#poisson-distributed-or-count-data) to determine sample sizes.
|
||||
|
||||
```
|
||||
exposure = 4 / (sqrt(lambda1) - sqrt(lambda2))^2
|
||||
```
|
||||
|
||||
where lambda1 is the baseline count data we've seen for the past two weeks,
|
||||
and lambda2 is `baseline count + mde*(baseline count)`.
|
||||
|
||||
`mde` is the minimum acceptable improvement you choose in the UI.
|
||||
|
||||
For funnel experiments, we use the general [Sample Size Determination](https://en.wikipedia.org/wiki/Sample_size_determination) formula, with 80% power and 5% significance. The formula then becomes:
|
||||
|
||||
```
|
||||
sample size per variant = 16 * conversion_rate * (1 - conversion_rate) / (mde)^2
|
||||
```
|
||||
|
||||
where `mde` is again the minimum detectable effect chosen in the UI.
|
||||
|
||||
We give these values as an estimate for how long to run the experiment. It's possible to end experiments before you reach the end, if you see an outsized effect.
|
||||
|
||||
Note how the recommended sample size in each case is inversely related to the minimum acceptable improvement. This makes sense, since the smaller the `mde`, the more sensitive your experiment is, and the more data you need to judge significance.
|
||||
|
||||
## Bayesian A/B testing
|
||||
|
||||
We follow a mostly bayesian approach to A/B testing. While running any experiment, we calculate two parameters: (1) Probability of each variant being the best, and (2) whether the results are significant or not.
|
||||
|
||||
Below are calculations for each kind of experiment.
|
||||
|
||||
## Trend experiment calculations
|
||||
|
||||
Trend experiments capture count data. For example, if you want to measure the change in total count of clicks, you'd use this kind of experiment.
|
||||
|
||||
We use Monte Carlo simulations to determine the probability of each variant being the best. Every variant can be simulated as a gamma distribution with shape parameter = trend count, and exposure = relative exposure for this variant.
|
||||
|
||||
Then, for each variant, we can sample from their distributions and get a count value for each of them.
|
||||
|
||||
The probability of a variant being the best is given by:
|
||||
|
||||

|
||||
|
||||
For calculating significance, we currently measure p-values using a poisson means test. [Here's a good primer of the formula](https://www.evanmiller.org/statistical-formulas-for-programmers.html#count_test). Results are significant when the p-value is less than 0.05
|
||||
|
||||
## Trend experiment exposure
|
||||
|
||||
Since count data can be over a total count, vs. the number of unique users, we use a proxy metric to measure exposure: The number of times `$feature_flag_called` event returns `control` or `test` is the respective exposure for the variant. This event is sent automatically when you do: `posthog.getFeatureFlag()`.
|
||||
|
||||
It's possible that a variant showing fewer count data can have higher probability, if its exposure is much smaller as well.
|
||||
|
||||
## Funnel experiment calculations
|
||||
|
||||
Funnel experiments capture conversion rates. For example, if you want to measure the change in conversion rate for buying a subscription to your site, you'd use this kind of experiment.
|
||||
|
||||
We use monte carlo simulations to determine the probability of each variant being the best. Every variant can be simulated as a beta distribution with alpha parameter = number of conversions, and beta parameter = number of failures, for this variant.
|
||||
|
||||
Then, for each variant, we can sample from their distributions and get a conversion rate for each of them.
|
||||
|
||||
The probability of a variant being the best is given by:
|
||||
|
||||

|
||||
|
||||
To calculate significance, we calculate the expected loss, as first mentioned in [VWO's SmartStats whitepaper](https://vwo.com/downloads/VWO_SmartStats_technical_whitepaper.pdf).
|
||||
|
||||
To do this, we again run a monte carlo simulation, and calculate loss as:
|
||||
|
||||

|
||||
|
||||
This represents the expected loss in conversion rate if you chose any other variant. If this is below 1%, we declare results as significant.
|
||||
|
||||
## How do we handle statistical significance?
|
||||
|
||||
For your results and conclusions to be valid, any experiment must have a significant exposure. For instance, if you test a product change and only one user sees the change, you can't extrapolate from that single user that the change will be beneficial/detrimental for your entire user base. This is true for any experiment that is a simple randomized controlled experiment (e.g. this is also done when testing new drugs or vaccines).
|
||||
|
||||
Furthermore, even a large sample size (e.g. approx. 10,000 participants) can result in ambiguous results. If, for example, the difference in conversion rate between the variants is less than 1%, it's hard to say whether one variant is truly better than the other. To be significant, there must be enough difference between the conversion rates, given the exposure size.
|
||||
|
||||
PostHog computes this significance for you automatically - we will let you know if your experiment has reached significant results or not. Once your experiment reaches significant results, it's safe to use those results to reach a conclusion and terminate the experiment. You can read more about how we do this in our 'Advanced' section below.
|
||||
|
||||
In the early days of an experiment, data can vary wildly, and sometimes one variant can seem overwhelmingly better. In this case, our significance calculations might say that the results are significant, but this shouldn't be the case, since we need more data.
|
||||
|
||||
Thus, before we hit 100 participants for each variant in an experiment, we default to results being not significant. Further, if the probability of the winning variant is less than 90%, we default to results being not significant.
|
||||
|
||||
So, you'll only see the green significance banner when all 3 conditions are met:
|
||||
|
||||
1. Each variant has >100 unique users
|
||||
2. The calculations above declare significance
|
||||
3. The probability of being the best > 90%.
|
||||
174
contents/docs/feature-flags/local-evaluation.mdx
Normal file
174
contents/docs/feature-flags/local-evaluation.mdx
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: Local evaluation
|
||||
availability:
|
||||
free: full
|
||||
selfServe: full
|
||||
enterprise: full
|
||||
---
|
||||
|
||||
There is a delay between loading the library and feature flags becoming available to use. This can be detrimental if you want to do something like redirecting to a different page based on a feature flag.
|
||||
|
||||
To have your feature flags available immediately, you can bootstrap them with a distinct user ID and their values during initialization.
|
||||
|
||||
```js
|
||||
posthog.init('<ph_project_api_key>', {
|
||||
api_host: '<ph_instance_address>',
|
||||
bootstrap: {
|
||||
distinctID: 'your-anonymous-id',
|
||||
featureFlags: {
|
||||
'flag-1': true,
|
||||
'variant-flag': 'control',
|
||||
'other-flag': false,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
To get the flag values for bootstrapping, you can call `getAllFlags()` in your server-side library, then pass the values to your frontend initialization. If you don't do this, your bootstrap values might be different than the values PostHog provides.
|
||||
|
||||
If the distinct user ID is an identified ID (the value you called `posthog.identify()` with), you can also pass the `isIdentifiedID` option. This ensures this ID is treated as an identified ID in the library. This is helpful as it warns you when you try to do something wrong with this ID, like calling identify again.
|
||||
|
||||
```js
|
||||
posthog.init('<ph_project_api_key>', {
|
||||
api_host: '<ph_instance_address>',
|
||||
bootstrap: {
|
||||
distinctID: 'your-identified-id',
|
||||
isIdentifiedID: true,
|
||||
featureFlags: {
|
||||
'flag-1': true,
|
||||
'variant-flag': 'control',
|
||||
'other-flag': false,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Forcing feature flags to update
|
||||
|
||||
In our client-side JavaScript library, we store flags as a cookie to reduce the load on the server and improve the performance of your app. This prevents always needing to make an HTTP request, flag evaluation can simply refer to data stored locally in the browser. This is known as 'local evaluation.'
|
||||
|
||||
While this makes your app faster, it means if your user does something mid-session which causes the flag to turn on for them, this does not immediately update. As such, if you expect your app to have scenarios like this _and_ you want flags to update mid-session, you can reload them yourself, by using the `reloadFeatureFlags` function.
|
||||
|
||||
```js
|
||||
posthog.reloadFeatureFlags()
|
||||
```
|
||||
|
||||
Calling this function forces PostHog to hit the endpoint for the updated information, and ensures changes are reflected mid-session.
|
||||
|
||||
## Server-side local evaluation
|
||||
|
||||
If you're using our server-side libraries, you can use local evaluation to improve performance instead of making additional API requests. This requires:
|
||||
|
||||
1. knowing and passing in all the person or group properties the flag relies on
|
||||
2. initializing the library with your personal API key (created in your account settings)
|
||||
|
||||
Local evaluation, in practice, looks like this:
|
||||
|
||||
<MultiLanguage>
|
||||
|
||||
```js
|
||||
await client.getFeatureFlag(
|
||||
'beta-feature',
|
||||
'distinct id',
|
||||
{
|
||||
personProperties: {'is_authorized': True}
|
||||
}
|
||||
)
|
||||
# returns string or None
|
||||
```
|
||||
|
||||
```python
|
||||
posthog.get_feature_flag(
|
||||
'beta-feature',
|
||||
'distinct id',
|
||||
person_properties={'is_authorized': True}
|
||||
)
|
||||
# returns string or None
|
||||
```
|
||||
|
||||
```php
|
||||
PostHog::getFeatureFlag(
|
||||
'beta-feature',
|
||||
'some distinct id',
|
||||
[],
|
||||
["is_authorized" => true]
|
||||
)
|
||||
// the third argument is for groups
|
||||
```
|
||||
|
||||
```ruby
|
||||
posthog.get_feature_flag(
|
||||
'beta-feature',
|
||||
'distinct id',
|
||||
person_properties: {'is_authorized': True}
|
||||
)
|
||||
# returns string or Nil
|
||||
```
|
||||
|
||||
```go
|
||||
enabledVariant, err := client.GetFeatureFlag(
|
||||
FeatureFlagPayload{
|
||||
Key: "multivariate-flag",
|
||||
DistinctId: "distinct-id",
|
||||
PersonProperties: posthog.NewProperties().
|
||||
Set("is_authorized", true),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
</MultiLanguage>
|
||||
|
||||
This works for `getAllFlags` as well. It evaluates all flags locally if possible, and if not, falls back to making a `decide` HTTP request.
|
||||
|
||||
<MultiLanguage>
|
||||
|
||||
```node
|
||||
await client.getAllFlags('distinct id', {
|
||||
groups: {},
|
||||
personProperties: { is_authorized: True },
|
||||
groupProperties: {},
|
||||
})
|
||||
// returns dict of flag key and value pairs.
|
||||
```
|
||||
|
||||
```php
|
||||
PostHog::getAllFlags('distinct id', ["organisation" => "some-company"], [], ["organisation" => ["is_authorized" => true]])
|
||||
```
|
||||
|
||||
```go
|
||||
featureVariants, _ := client.GetAllFlags(FeatureFlagPayloadNoKey{
|
||||
DistinctId: "distinct-id",
|
||||
})
|
||||
```
|
||||
|
||||
```python
|
||||
posthog.get_all_flags('distinct id', groups={}, person_properties={'is_authorized': True}, group_properties={})
|
||||
# returns dict of flag key and value pairs.
|
||||
```
|
||||
|
||||
```ruby
|
||||
posthog.get_all_flags('distinct id', groups: {}, person_properties: {'is_authorized': True}, group_properties: {})
|
||||
# returns hash of flag key and value pairs.
|
||||
```
|
||||
|
||||
</MultiLanguage>
|
||||
|
||||
## Using locally
|
||||
|
||||
To test feature flags locally, you can open your developer tools and override the feature flags. You will get a warning that you're manually overriding feature flags.
|
||||
|
||||
```js
|
||||
posthog.feature_flags.override(['feature-flag-1', 'feature-flag-2'])
|
||||
```
|
||||
|
||||
This will persist until you call override again with the argument `false`:
|
||||
|
||||
```js
|
||||
posthog.feature_flags.override(false)
|
||||
```
|
||||
|
||||
To see the feature flags that are currently active for you, you can call:
|
||||
|
||||
```js
|
||||
posthog.feature_flags.getFlags()
|
||||
```
|
||||
144
contents/docs/feature-flags/manual.mdx
Normal file
144
contents/docs/feature-flags/manual.mdx
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
title: Feature Flags
|
||||
related:
|
||||
- ../../tutorials/experiments.md
|
||||
- ../../tutorials/metrics-tutorial.md
|
||||
- ../../tutorials/canary-release.md
|
||||
- ../../tutorials/cohorts.md
|
||||
availability:
|
||||
free: partial
|
||||
selfServe: full
|
||||
enterprise: full
|
||||
|
||||
features:
|
||||
multivariate:
|
||||
free: false
|
||||
selfServe: true
|
||||
enterprise: true
|
||||
---
|
||||
|
||||
Feature Flags enable you to safely deploy and roll back new features. This means you can ship the code for new features and roll it out to your users in a managed way. If something goes wrong, you can roll back without having to re-deploy your application.
|
||||
|
||||
Feature Flags also help you control access to certain parts of your product, such as only showing paid features to users with an active subscription.
|
||||
|
||||
## Creating feature flags
|
||||
|
||||
In the PostHog app sidebar, go to 'Feature Flags' and click 'New feature flag'.
|
||||
|
||||
Think of a descriptive name and select how you want to roll out your feature.
|
||||
|
||||

|
||||
|
||||
## Implementing the feature flag
|
||||
|
||||
When you create a feature flag, we'll show you an example snippet. It will look something like this:
|
||||
|
||||
<MultiLanguage>
|
||||
|
||||
```js
|
||||
if (posthog.isFeatureEnabled('new-beta-feature')) {
|
||||
// run your activation code here
|
||||
}
|
||||
```
|
||||
|
||||
```node
|
||||
const isMyFlagEnabledForUser = await client.isFeatureEnabled('new-beta-feature', 'user distinct id')
|
||||
|
||||
if (isMyFlagEnabledForUser) {
|
||||
// Do something differently for this user
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
if (PostHog::isFeatureEnabled('new-beta-feature', 'user distinct id')) {
|
||||
// do something here
|
||||
}
|
||||
```
|
||||
|
||||
```ruby
|
||||
is_my_flag_enabled = posthog.is_feature_enabled('new-beta-feature', 'user distinct id')
|
||||
|
||||
if is_my_flag_enabled
|
||||
# Do something differently for this user
|
||||
end
|
||||
```
|
||||
|
||||
```go
|
||||
isFlagEnabledForUser, err := client.IsFeatureEnabled(
|
||||
FeatureFlagPayload{
|
||||
Key: "new-beta-feature",
|
||||
DistinctId: "distinct-id",
|
||||
})
|
||||
|
||||
if (isFlagEnabledForUser) {
|
||||
// Do something differently for this user
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
if posthog.feature_enabled("new-beta-feature", "user_distinct_id"):
|
||||
runAwesomeFeature()
|
||||
```
|
||||
|
||||
```
|
||||
curl https://app.posthog.com/decide/ \
|
||||
-X POST -H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"api_key": "phc_Wg2jMXmDvsnrQ3IKuVyspZghFIVE2IbxuqVYlhm7ry0",
|
||||
"distinct_id": "[user distinct id]"
|
||||
}'
|
||||
```
|
||||
|
||||
</MultiLanguage>
|
||||
|
||||
What you do inside that if statement is up to you. You might change the CSS of a button, hide an entire section, or move elements around on the page.
|
||||
|
||||
### Ensuring flags are loaded before usage
|
||||
|
||||
Every time a user loads a page we send a request in the background to an endpoint to get the feature flags that apply to that user. In the client, we store those flags as a cookie.
|
||||
|
||||
This means that for most page views the feature flags will be available immediately, _except_ for the first time a user visits.
|
||||
|
||||
To combat that, there's a JavaScript callback you can use to wait for the flags to come in:
|
||||
|
||||
```js
|
||||
posthog.onFeatureFlags(function () {
|
||||
// feature flags are guaranteed to be available at this point
|
||||
if (posthog.isFeatureEnabled('new-beta-feature')) {
|
||||
// do something
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Persisting feature flags across authentication steps
|
||||
|
||||
You have an option to persist flags across authentication steps.
|
||||
|
||||
Consider this case: An anonymous person comes to your website and you use a flag to show them a green call to action.
|
||||
|
||||
Without persisting feature flags, the flag value can change on login because their identity can change (from anonymous to identified). Once they login, the flag might evaluate differently and show a red call to action instead.
|
||||
|
||||
This usually is not a problem since experiments run either completely for anonymous users, or completely for logged in users.
|
||||
|
||||
However, with some businesses, like e-commerce, it's very common to browse things anonymously and login right before checking out. In cases like these you can preserve the feature flag values by checking this checkbox.
|
||||
|
||||

|
||||
|
||||
Note that there are some performance trade-offs here. Specifically,
|
||||
|
||||
1. Enabling this slows down the feature flag response.
|
||||
2. It disables local evaluation of the feature flag.
|
||||
3. It disables bootstrapping this feature flag.
|
||||
|
||||
## Further reading
|
||||
|
||||
Want to know more about what's possible with Feature Flags in PostHog? Try these tutorials:
|
||||
|
||||
- [How to do a canary release with feature flags](/tutorials/canary-release)
|
||||
- [Running experiments on new users](/tutorials/new-user-experiments)
|
||||
- [How to run Experiments without feature flags](/tutorials/experiments)
|
||||
- Tips for [feature flag best practices with examples](/blog/feature-flag-best-practices)
|
||||
|
||||
Using a library other than JavaScript for your feature flag implementation? Check out [these other libraries](/docs/integrate/libraries) for more details.
|
||||
|
||||
Want more? Check our [full list of PostHog tutorials](/tutorials).
|
||||
77
contents/docs/feature-flags/multivariate-flags.mdx
Normal file
77
contents/docs/feature-flags/multivariate-flags.mdx
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Multivariate feature flags
|
||||
availability:
|
||||
free: none
|
||||
selfServe: full
|
||||
enterprise: full
|
||||
---
|
||||
|
||||
> Multivariate feature flags are only available when using PostHog >= 1.28 (if self-hosting) and [posthog-js](https://github.com/PostHog/posthog-js) >= 1.13.
|
||||
|
||||
PostHog 1.28 introduced support for multivariate feature flags which can return string values according to a specified distribution.
|
||||
|
||||
Some examples for a 3-variant case would be 33/33/34%, 50/25/25%, or 70/20/10%. This is ideal for when you want to test multiple variants of the same interchangeable content, such as marketing taglines, colors, or page layouts.
|
||||
|
||||
## Creating a feature flag with multiple variants
|
||||
|
||||
Create a multivariate feature flag just like you would a standard flag, and then change the "Served value" option to "a string value". You will then be prompted to enter a few keys with optional descriptions and set the distribution percentages for each.
|
||||
|
||||
Note that the rollout percentage of feature flag variants must add up to 100%. If you wish to exclude some users from your test, i.e. have some users receive no value at all, configure the _release condition groups_. While the release condition groups determine how many users will be bucketed into _any_ of the given variants, the rollout percentage of each variant determines the portion of the overall release group that will be assigned to that particular variant.
|
||||
|
||||
## Using multivariate feature flags in your code
|
||||
|
||||
With the latest version of our JS library, you can call:
|
||||
|
||||
```js
|
||||
if (posthog.getFeatureFlag('checkout-button-color') === 'black') {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
`getFeatureFlag` also returns true or false for standard (Boolean) feature flags, meaning that the following statements are equivalent:
|
||||
|
||||
```js
|
||||
posthog.isFeatureEnabled('new-beta-feature')
|
||||
posthog.getFeatureFlag('new-beta-feature') === true
|
||||
```
|
||||
|
||||
## `getFlagVariants`
|
||||
|
||||
Just as you can call `getFlags()` to return an array of feature flags that are currently active, you can call:
|
||||
|
||||
```js
|
||||
posthog.feature_flags.getFlagVariants()
|
||||
```
|
||||
|
||||
`getFlagVariants` returns an object:
|
||||
|
||||
```json
|
||||
{
|
||||
"new-beta-feature": true,
|
||||
"checkout-button-color": "black"
|
||||
}
|
||||
```
|
||||
|
||||
## `onFeatureFlags`
|
||||
|
||||
`onFeatureFlags(callback)` now passes the feature flag variants object as the second argument to `callback`, which looks like this:
|
||||
|
||||
```js
|
||||
posthog.onFeatureFlags(function (flags, flagVariants) {
|
||||
// do something useful
|
||||
console.log(flags) // ["new-beta-feature", "checkout-button-color"]
|
||||
console.log(flagVariants) // { "new-beta-feature": true, "checkout-button-color": "black" }
|
||||
})
|
||||
```
|
||||
|
||||
Note that `getFlags()` and the callback argument `flags` will include the key names of all truthy feature flags, including active multivariate feature flags.
|
||||
|
||||
## Querying data by multivariate feature flag values
|
||||
|
||||
With the latest version of our JS library, we send each feature flag's value as a separate property on every event. This means the values can be used in filters and breakdowns in Insights queries or wherever else you may choose to filter incoming events.
|
||||
|
||||
We send the event properties as `$feature/your-feature-name`, for example `$feature/checkout-button-color`. Standard (Boolean) flags are captured in this format as well.
|
||||
|
||||
For example, if you have a Trends graph of button click events and you'd like to narrow it down to clicks only when the checkout button is blue, apply a filter to your graph series such that `$feature/checkout-button-color = blue`.
|
||||
|
||||
If you'd like to compare all variants for which we have data in one graph, apply a breakdown by `$feature/checkout-button-color`.
|
||||
39
contents/docs/feature-flags/rollout-strategies.mdx
Normal file
39
contents/docs/feature-flags/rollout-strategies.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Rollout strategies
|
||||
---
|
||||
|
||||
There are three options for deciding who sees your new feature. You can roll out the feature to:
|
||||
|
||||
1. A fixed percentage of users or groups
|
||||
1. A set of users or groups filtered based on their user properties, cohort (based on user properties), or group properties.
|
||||
1. A combination of the two
|
||||
|
||||
## Roll out to a percentage of users or groups
|
||||
|
||||
By rolling out to a percentage of users or groups, you can gradually ramp up those who sees a new feature. To calculate this, we "hash" a combination of the key of the feature flag and the unique distinct ID of the user.
|
||||
|
||||
This way a user always falls in the same place between 0 and 100%, so they consistently see or do not see the feature controlled by the flag. As you move the slider towards 100%, more users start seeing your feature.
|
||||
|
||||
Hashing also means that the same user falls along different points of the line for each new feature. For example, a user may start seeing the feature at 5% for feature A, but only at 80% for feature B.
|
||||
|
||||
## Filter by user or group properties
|
||||
|
||||
This works just like any other filter in PostHog. You can select any property and users that match those filters will see your new feature.
|
||||
|
||||
By combining properties and percentages, you can determine something like:
|
||||
|
||||
- Roll out this feature to 80% of users that have an email set
|
||||
- Provide access to this feature to 25% of organizations where the `beta-tester` property is `true`.
|
||||
- Show this component to 10% of users whose `signed_up_at` date is after January 1st.
|
||||
|
||||
## De-activating properties
|
||||
|
||||
If the feature has caused a problem, or you don't need the feature flag anymore, you can disable it instantly and completely. Doing so ensures _no users_ will have the flag enabled.
|
||||
|
||||
## Feature flag persistence
|
||||
|
||||
For feature flags that filter by user properties only, a given flag will always be on if a certain user meets all the specified property filters.
|
||||
|
||||
However, for flags using a rollout percentage mechanism (either by itself or in combination with user properties), the flag will persist for a given user as long as the rollout percentage and the flag key are not changed.
|
||||
|
||||
As a result, bear in mind that changing those values will result in flags being toggled on and off for certain users in a non-predictable way.
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Actions and insights
|
||||
title: Actions & insights
|
||||
nextPage: ./group-analytics.mdx
|
||||
featuredImage: ./images/docs-actions.png
|
||||
---
|
||||
|
||||
Actions and insights are the two foundational analysis tools within PostHog.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Group analytics
|
||||
nextPage: ./next-steps.mdx
|
||||
featuredImage: ./images/docs-groups.png
|
||||
---
|
||||
|
||||
import CreateGroupType from "./\_snippets/group-type-create.mdx"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: Identify users
|
||||
nextPage: ./actions-and-insights.mdx
|
||||
featuredImage: ./images/docs-identify.png
|
||||
---
|
||||
|
||||
import IdentifyUserBackend from "./\_snippets/identify-user-backend.mdx"
|
||||
|
||||
PostHog allows you to identify your users with an ID of your choice. This enables PostHog to associate events with specific users, track them on different platforms, and connect events from before and after users log in for the first time.
|
||||
PostHog allows you to identify your users with an ID of your choice. This enables PostHog to associate events with a specific user, track them on different platforms, and connect events from before and after they log in for the first time.
|
||||
|
||||
All events within PostHog are associated with a specific person, either an **Anonymous person** or an **Identified person**, typically based on whether they're logged in to your application or not.
|
||||
|
||||
@@ -42,7 +43,7 @@ On the client side, this is done by calling `identify` with the same `distinct_i
|
||||
|
||||
```js
|
||||
// Using the 'distinct_id' returned to us from the server
|
||||
posthog.identify('distinct_id')
|
||||
posthog.identify('my_user_12345')
|
||||
```
|
||||
|
||||
Calling `identify` from the frontend will do two things:
|
||||
@@ -51,6 +52,9 @@ Calling `identify` from the frontend will do two things:
|
||||
2. All future events will be associated with this new user (`distinct_id`), even if we still use their anonymous ID
|
||||
|
||||
Effectively, these two users have been merged into one.
|
||||
|
||||
From now on, all events PostHog sees with ID `17b845b08de74-033c497ed2753c-35667c03-1fa400-17b845b08dfd55` (anonymous ID) will be attributed to the person with ID `my_user_12345`. This person now has 2 distinct IDs, and either of them can be used to reference the same person.
|
||||
|
||||
By combining our anonymous user with our newly created user, we can answer important questions about our onboarding flow such as conversion rate and total unique users.
|
||||
|
||||
## 3. Identifying logged-in users
|
||||
@@ -61,7 +65,7 @@ In most cases, all we need to do is call `identify` whenever they return back to
|
||||
|
||||
```js
|
||||
// The same 'distinct_id' as before
|
||||
posthog.identify('distinct_id', {
|
||||
posthog.identify('my_user_12345', {
|
||||
name: 'Max Hedgehog',
|
||||
email: 'max@hedgehogmail.com',
|
||||
// ... any other user properties
|
||||
@@ -121,3 +125,98 @@ posthog.alias('user-id', 'non-identified-id')
|
||||
|
||||
posthog.capture('user-id', '$merge_dangerously', {'alias': 'second-user-id'})
|
||||
```
|
||||
|
||||
## Considerations
|
||||
|
||||
Identifying users is a powerful feature, but it also has the potential to create problems if misused.
|
||||
|
||||
An important mistake to avoid is using non-unique distinct IDs to identify users. Two common ways in which this can happen are:
|
||||
|
||||
- Your logic for generating IDs does not generate sufficiently strong IDs and you can end up with a clash where 2 users have the same ID
|
||||
- There's a bug, typo, or mistake in your code leading to most or all users being identified with generic IDs like `null`, `true`, or `distinctId`
|
||||
|
||||
All of the above scenarios are highly problematic, as they will cause distinct users to be merged together in PostHog.
|
||||
|
||||
While implementing analytics with PostHog, make sure you avoid above pitfalls to maintain data integrity.
|
||||
|
||||
PostHog also has a few built-in protections stopping the most common threats to data integrity:
|
||||
|
||||
- We do not allow identifying users with the following IDs (case insensitive):
|
||||
- `anonymous`
|
||||
- `guest`
|
||||
- `distinctid`
|
||||
- `distinct_id`
|
||||
- `id`
|
||||
- `not_authenticated`
|
||||
- `email`
|
||||
- `undefined`
|
||||
- `true`
|
||||
- `false`
|
||||
- We do not allow identifying users with the following IDs (case sensitive):
|
||||
- `[object Object]`
|
||||
- `NaN`
|
||||
- `None`
|
||||
- `none`
|
||||
- `null`
|
||||
- `0`
|
||||
- We do not allow identifying users with empty space strings of any length (`' '`, `' '`, etc.)
|
||||
- We do not allow merging from an already identified user (`distinct_id` user can be previously identified, but `anon_distinct_id` and `alias` user cannot).
|
||||
|
||||
If we encounter an `$identify` or `$create_alias` event with one of the above problems, the following will happen:
|
||||
|
||||
- We process the event normally (it will be ingested and show up in the UI)
|
||||
- We refuse to merge users and an ingestion warning will be logged (see [ingestion warnings](/manual/data-management#ingestion-warnings) for more details).
|
||||
- The event will be only be tied to user behind the first passed `distinct_id`
|
||||
|
||||
## Filtering internal users
|
||||
|
||||
If you want to avoid tracking users within your organization, [you can do this](/tutorials/filter-internal-users) within your project's settings.
|
||||
|
||||
## Signup flow with frontend and backend
|
||||
|
||||
To use PostHog effectively we want all of the events tied to the same user to be tied to the same `person_id` (see [consequences of merging users](/docs/how-posthog-works/ingestion-pipeline#consequences-of-merging)).
|
||||
|
||||
For when a user signs up to your service you may trigger some events on the frontend and the backend. The key is to make sure that **both frontend and backend use the same distinctId at least once.**
|
||||
|
||||
### Example login flow
|
||||
|
||||
On the backend (example with Node.JS) you receive the signup / login code and track the user
|
||||
|
||||
```js
|
||||
const user = await createUser();
|
||||
posthog.identify({
|
||||
distinctId: user.id,
|
||||
properties: {
|
||||
email: user.email
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
On the frontend you need to have the same ID passed down in order to link the two users
|
||||
```js
|
||||
const user = await fetch("/api/users/@me")
|
||||
posthog.identify(user.id)
|
||||
```
|
||||
|
||||
If you use a different identifier or multiple identifiers, be sure to alias the two IDs together for example on the backend with `posthog-node`
|
||||
```js
|
||||
posthog.alias({
|
||||
distinctId: user.id,
|
||||
alias: user.alternativeId,
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### Things to be aware of
|
||||
|
||||
There's a few things to keep in mind when using a sign-up flow that involves both the frontend and backend:
|
||||
1. We have an event buffer to delay creating persons from backend events (see [all about the event buffer](/docs/how-posthog-works/ingestion-pipeline#all-about-the-event-buffer)) that will help.
|
||||
2. The event buffer has a limited time window, so the (identify or alias) event that merges the frontend and backend user should come in within that window (60s)
|
||||
3. We don't buffer `$identify` events, so from the backend take care to not send those for setting properties before the users are merged. For setting user properties you can use any custom event, e.g.
|
||||
```python
|
||||
posthog.capture(
|
||||
'distinct id',
|
||||
event='movie played',
|
||||
properties={ '$set': { 'userProperty': 'value' } }
|
||||
)
|
||||
```
|
||||
BIN
contents/docs/getting-started/images/docs-actions.png
Normal file
BIN
contents/docs/getting-started/images/docs-actions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
contents/docs/getting-started/images/docs-groups.png
Normal file
BIN
contents/docs/getting-started/images/docs-groups.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
contents/docs/getting-started/images/docs-identify.png
Normal file
BIN
contents/docs/getting-started/images/docs-identify.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
contents/docs/getting-started/images/docs-install.png
Normal file
BIN
contents/docs/getting-started/images/docs-install.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
contents/docs/getting-started/images/docs-send-events.png
Normal file
BIN
contents/docs/getting-started/images/docs-send-events.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
contents/docs/getting-started/images/docs-user-properties.png
Normal file
BIN
contents/docs/getting-started/images/docs-user-properties.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Install PostHog
|
||||
nextPage: ./send-events.mdx
|
||||
featuredImage: ./images/docs-install.png
|
||||
---
|
||||
|
||||
Once your PostHog instance is up and running, the next step is to get PostHog installed.
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
---
|
||||
title: Next steps
|
||||
isArticle: false
|
||||
hideAnchor: true
|
||||
---
|
||||
|
||||
import { ProductAnalytics, FeatureFlags, Experiments, Apps } from "components/GettingStarted"
|
||||
import { ProductAnalytics, FeatureFlags, Experiments, Apps, SessionRecording, Data } from "components/GettingStarted"
|
||||
|
||||
Now that PostHog is installed and configured, it's time to explore what you can do with it.
|
||||
|
||||
<ProductAnalytics />
|
||||
|
||||
<SessionRecording />
|
||||
|
||||
<FeatureFlags />
|
||||
|
||||
<Experiments />
|
||||
|
||||
<Data />
|
||||
|
||||
<Apps />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Send events
|
||||
nextPage: ./identify-users.mdx
|
||||
featuredImage: ./images/docs-send-events.png
|
||||
---
|
||||
|
||||
import SendEventBackend from "./\_snippets/send-event-backend.mdx"
|
||||
|
||||
@@ -6,7 +6,7 @@ isArticle: false
|
||||
hideAnchor: true
|
||||
---
|
||||
|
||||
import { InstallChapter, SendEventsChapter, IdentifyUsersChapter, ActionsAndInsightsChapter, GroupAnalyticsChapter } from "components/GettingStarted"
|
||||
import { InstallChapter, SendEventsChapter, IdentifyUsersChapter, UserPropertiesChapter, ActionsAndInsightsChapter, GroupAnalyticsChapter } from "components/GettingStarted"
|
||||
import SignupLink from "components/SignupLink"
|
||||
|
||||
<p class="text-black/70 dark:text-white/90 font-bold">👋 Welcome! This guide will get you set up with PostHog for your web app and backend.</p>
|
||||
@@ -21,6 +21,8 @@ import SignupLink from "components/SignupLink"
|
||||
|
||||
<IdentifyUsersChapter />
|
||||
|
||||
<UserPropertiesChapter />
|
||||
|
||||
<ActionsAndInsightsChapter />
|
||||
|
||||
<GroupAnalyticsChapter />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: User properties
|
||||
sidebarTitle: User properties
|
||||
featuredImage: ./images/docs-user-properties.png
|
||||
sidebar: Docs
|
||||
showTitle: true
|
||||
---
|
||||
@@ -1,116 +0,0 @@
|
||||
---
|
||||
title: Identifying users
|
||||
sidebarTitle: Identifying users
|
||||
sidebar: Docs
|
||||
showTitle: true
|
||||
---
|
||||
|
||||
PostHog allows you to identify your users with an ID of your choice so you can track users across platforms, connect events from before and after users log in, and leverage your preferred type of ID for filtering through your users.
|
||||
|
||||
Identifying users is usually done via our libraries, by calling the `identify` or `alias` method. This method will associate an **anonymous ID** with a distinct ID of your choice. In client libraries, the anonymous ID is stored locally and inferred for you, but in server-side libraries you need to tell PostHog what that anonymous ID is too.
|
||||
|
||||
For example, if you identify an user in your website as follows:
|
||||
|
||||
```js
|
||||
// posthog-js
|
||||
posthog.identify('my_user_12345')
|
||||
```
|
||||
|
||||
PostHog will then pull their anonymous ID (e.g. `17b845b08de74-033c497ed2753c-35667c03-1fa400-17b845b08dfd55`) and associate it with the ID you passed in (`my_user_12345`).
|
||||
|
||||
From now on, all events PostHog sees with ID `17b845b08de74-033c497ed2753c-35667c03-1fa400-17b845b08dfd55` will be attributed to the person with ID `my_user_12345`. This person now has 2 distinct IDs, and either of them can be used to reference the same person.
|
||||
|
||||
## Considerations
|
||||
|
||||
Identifying users is a powerful feature, but it also has the potential to create problems if misused.
|
||||
|
||||
An important mistake to avoid is using non-unique distinct IDs to identify users. Two common ways in which this can happen are:
|
||||
|
||||
- Your logic for generating IDs does not generate sufficiently strong IDs and you can end up with a clash where 2 users have the same ID
|
||||
- There's a bug, typo, or mistake in your code leading to most or all users being identified with generic IDs like `null`, `true`, or `distinctId`
|
||||
|
||||
All of the above scenarios are highly problematic, as they will cause distinct users to be merged together in PostHog.
|
||||
|
||||
While implementing analytics with PostHog, make sure you avoid above pitfalls to maintain data integrity.
|
||||
|
||||
PostHog also has a few built-in protections stopping the most common threats to data integrity:
|
||||
|
||||
- We do not allow identifying users with the following IDs (case insensitive):
|
||||
- `anonymous`
|
||||
- `guest`
|
||||
- `distinctid`
|
||||
- `distinct_id`
|
||||
- `id`
|
||||
- `not_authenticated`
|
||||
- `email`
|
||||
- `undefined`
|
||||
- `true`
|
||||
- `false`
|
||||
- We do not allow identifying users with the following IDs (case sensitive):
|
||||
- `[object Object]`
|
||||
- `NaN`
|
||||
- `None`
|
||||
- `none`
|
||||
- `null`
|
||||
- `0`
|
||||
- We do not allow identifying users with empty space strings of any length (`' '`, `' '`, etc.)
|
||||
- We do not allow merging from an already identified user (`distinct_id` user can be previously identified, but `anon_distinct_id` and `alias` user cannot).
|
||||
|
||||
If we encounter an `$identify` or `$create_alias` event with one of the above problems, the following will happen:
|
||||
|
||||
- We process the event normally (it will be ingested and show up in the UI)
|
||||
- We refuse to merge users and an ingestion warning will be logged (see [ingestion warnings](/manual/data-management#ingestion-warnings) for more details).
|
||||
- The event will be only be tied to user behind the first passed `distinct_id`
|
||||
|
||||
## Filtering internal users
|
||||
|
||||
If you want to avoid tracking users within your organization, [you can do this](/tutorials/filter-internal-users) within your project's settings.
|
||||
|
||||
## Signup flow with frontend and backend
|
||||
|
||||
To use PostHog effectively we want all of the events tied to the same user to be tied to the same `person_id` (see [consequences of merging users](/docs/how-posthog-works/ingestion-pipeline#consequences-of-merging)).
|
||||
|
||||
For when a user signs up to your service you may trigger some events on the frontend and the backend. The key is to make sure that **both frontend and backend use the same distinctId at least once.**
|
||||
|
||||
### Example login flow
|
||||
|
||||
On the backend (example with Node.JS) you receive the signup / login code and track the user
|
||||
|
||||
```js
|
||||
const user = await createUser();
|
||||
posthog.identify({
|
||||
distinctId: user.id,
|
||||
properties: {
|
||||
email: user.email
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
On the frontend you need to have the same ID passed down in order to link the two users
|
||||
```js
|
||||
const user = await fetch("/api/users/@me")
|
||||
posthog.identify(user.id)
|
||||
```
|
||||
|
||||
If you use a different identifier or multiple identifiers, be sure to alias the two IDs together for example on the backend with `posthog-node`
|
||||
```js
|
||||
posthog.alias({
|
||||
distinctId: user.id,
|
||||
alias: user.alternativeId,
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### Things to be aware of
|
||||
|
||||
There's a few things to keep in mind when using a sign-up flow that involves both the frontend and backend:
|
||||
1. We have an event buffer to delay creating persons from backend events (see [all about the event buffer](/docs/how-posthog-works/ingestion-pipeline#all-about-the-event-buffer)) that will help.
|
||||
2. The event buffer has a limited time window, so the (identify or alias) event that merges the frontend and backend user should come in within that window (60s)
|
||||
3. We don't buffer `$identify` events, so from the backend take care to not send those for setting properties before the users are merged. For setting user properties you can use any custom event, e.g.
|
||||
```python
|
||||
posthog.capture(
|
||||
'distinct id',
|
||||
event='movie played',
|
||||
properties={ '$set': { 'userProperty': 'value' } }
|
||||
)
|
||||
```
|
||||
68
contents/docs/session-recording/configure.mdx
Normal file
68
contents/docs/session-recording/configure.mdx
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Configure recording collection
|
||||
---
|
||||
|
||||
There are some configurations that can be used to adjust how recordings are captured.
|
||||
|
||||
| Attribute | Description |
|
||||
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `maskAllInputs`<br/><br/>**Type:** Boolean<br/>**Default:** `true` | When `true`, all data from user input fields will be replaced by '\*'s. |
|
||||
| `maskInputOptions`<br/><br/>**Type:** Object<br/>**Default:** `{}` | Only takes effect if `maskAllInputs` is `false`. It determines which specific input field types should be masked. An example value might be `{ password: true }`. Options are: `password`, `textarea`, `select`, `color`, `date`, `email`, `month`, `number`, `range`, `search`, `tel`, `text`, `time`, `url`, `week`, and `datetime-local`. |
|
||||
| `inlineStylesheet`<br/><br/>**Type:** Boolean<br/>**Default:** `true` | If `false`, stylesheets will not be included with the recording data. Rather, a URL pointing to the stylesheet will be included. Setting this to false will decrease the storage space used for recordings and improve playback buffering time, but it can also cause some flickering in the playback experience. |
|
||||
| `recordCanvas`<br/><br/>**Type:** Boolean<br/>**Default:** `false` | If `true`, canvas elements will be recorded by rrweb. |
|
||||
|
||||
To configure these options, pass them to your `posthog.init` call along with your other `posthog-js` [configurations](/docs/integrate/client/js#config). They go inside of the `session_recording` attribute, like so:
|
||||
|
||||
```js
|
||||
posthog.init('<ph_project_api_key>', {
|
||||
api_host: '<ph_instance_address>',
|
||||
session_recording: {
|
||||
inlineStylesheet: false,
|
||||
},
|
||||
// ... other options
|
||||
})
|
||||
```
|
||||
|
||||
## Console logs recording (beta)
|
||||
|
||||
PostHog can also capture console logs from your application. This is useful for debugging, providing extra context on what is happening in your users browser environment. As console logs can contain sensitive information we do not capture these logs automatically. You can enable this feature globally from your PostHog [project settings page](https://app.posthog.com/project/settings) or client-side by setting `enable_recording_console_log` in our [JavaScript library config](/docs/integrate/client/js/#config) to `true`.
|
||||
|
||||
Console logs will be recorded if _either_ the project setting **or** the client-side config is set to `true`. If recordings overall are disabled then console logs will also not be recorded.
|
||||
|
||||
```javascript
|
||||
posthog.init('<ph_project_api_key>', {
|
||||
api_host: '<ph_instance_address>',
|
||||
// REMINDER: This is only needed if you aren't using the project settings config
|
||||
enable_recording_console_log: true,
|
||||
// ... other options
|
||||
})
|
||||
```
|
||||
|
||||
## Further controls
|
||||
|
||||
If you want more granular controls, you can choose to enable session recording using [feature flags](feature-flags). This enables you to control session recordings based on users with certain previous events/actions or properties (or just to capture a percentage of all sessions).
|
||||
|
||||
To do this set `disable_session_recording` in our [JavaScript library config](/docs/integrate/client/js/#config) to `true`.
|
||||
|
||||
Then conditionally call the method `posthog.startSessionRecording` to enable it using the feature flag.
|
||||
|
||||
For example:
|
||||
|
||||
```javascript
|
||||
posthog.init('<ph_project_api_key>', {
|
||||
api_host: '<ph_instance_address>',
|
||||
disable_session_recording: true,
|
||||
// ... other options
|
||||
})
|
||||
window.posthog.onFeatureFlags(function () {
|
||||
if (window.posthog.isFeatureEnabled('your-feature-flag')) {
|
||||
window.posthog.startSessionRecording()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Equally you can stop the recording at any point via:
|
||||
|
||||
```
|
||||
posthog.stopSessionRecording()
|
||||
```
|
||||
20
contents/docs/session-recording/data-retention.mdx
Normal file
20
contents/docs/session-recording/data-retention.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Recordings data retention
|
||||
---
|
||||
|
||||
Depending on how your app or website is built, recordings can take a lot of disk space. To manage this, we have the following retention policy options in place.
|
||||
|
||||
## PostHog Cloud & Clickhouse Self-hosted
|
||||
|
||||
By default, recordings are automatically deleted after 3 weeks. Old recordings are deleted with Clickhouse's <a target="_blank" href="https://clickhouse.com/docs/en/sql-reference/statements/alter/ttl/">Table TTL</a>. If you're self-hosting, you can set your own retention policy by updating the `RECORDINGS_TTL_WEEKS` configuration on your instance settings page.
|
||||
|
||||
Please note, if your Clickhouse storage is nearing capacity, you'll want to temporarily increase your volume size before running the command above (even if you're decreasing the value). Otherwise, the command can hang.
|
||||
|
||||
## Legacy Postgres Self-hosted
|
||||
|
||||
Recordings are kept indefinitely by default. You can set recordings to delete after a configurable number of days in the [project settings page](/docs/user-guides/application-settings#project-settings).
|
||||
|
||||
## Preserving recordings
|
||||
|
||||
Any recordings you'd like to preserve for longer can be done by downloading them locally. Downloaded recordings can then be imported back into PostHog for future playback, even if the original recording has expired.
|
||||
Currently, it's only possible to save a single recording at a time.
|
||||
36
contents/docs/session-recording/manual.mdx
Normal file
36
contents/docs/session-recording/manual.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Enabling recording
|
||||
related:
|
||||
- ../../tutorials/funnels.md
|
||||
- ../../tutorials/nextjs-supabase-signup-funnel.mdx
|
||||
availability:
|
||||
free: full
|
||||
selfServe: full
|
||||
enterprise: full
|
||||
---
|
||||
|
||||
Session Recording allows you to record users navigating through your website and play back the individual sessions to watch how real users use your product.
|
||||
|
||||
## Enabling session recording
|
||||
|
||||
Recordings can only be used with our [JavaScript library](/docs/integrate/client/js) and requires the feature to be enabled in PostHog's Project Settings (`/project/settings`).
|
||||
Once enabled, the JS library will start recording sessions by default.
|
||||
|
||||
<blockquote class="warning-note">
|
||||
Session Recording does not work if you send data using Segment's SDK as this data is not collected. If you use
|
||||
Segment, you may want to add the PostHog library too (make sure to only send regular event data from one source).
|
||||
</blockquote>
|
||||
|
||||
Recordings can be toggled on and off in the JS library by appropriately setting the [config](/docs/integrate/client/js/#config). Users who opt out of event capturing will not have their sessions recorded.
|
||||
|
||||
To watch recordings, you can either visit the 'Recordings' page or click on any data point in an insight and from the list of persons related to that data point. This is specially useful in funnels, where you can drill down and watch recordings of users who converted or dropped off.
|
||||
|
||||
When watching recordings, you can change the speed as well as select the option 'skip inactive' - this will skip chunks of the recording where the user was inactive on the page.
|
||||
|
||||
## Ignoring sensitive elements
|
||||
|
||||
To avoid recording passwords or other sensitive information, the default PostHog settings do not capture any user input fields in recordings (the text is replaced by '\*'s). This setting can be adjusted by using the [recording configurations](/docs/user-guides/recordings#recording-configurations).
|
||||
|
||||
If your application displays sensitive user information outside of input fields, you need to update your codebase to prevent PostHog from capturing this information during session recordings.
|
||||
|
||||
To do so, you should add the CSS class name `ph-no-capture` to elements which should not be recorded. This will lead to the element being replaced with a block of the same size when you play back the recordings. Make sure everyone who watches recordings in your team is aware of this, so that they don't think your product is broken when something doesn't show up!
|
||||
66
contents/docs/session-recording/troubleshooting.mdx
Normal file
66
contents/docs/session-recording/troubleshooting.mdx
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Troubleshooting and FAQs
|
||||
---
|
||||
|
||||
Having trouble with recordings? Below are some tips for getting past some common issues
|
||||
|
||||
### My browser freezes / crashes when recordings are enabled
|
||||
|
||||
Some Cookie banners or "CMP"s modify the webpage in reaction to any changes to cookies, which can cause an infinite loop. If you are using a tool like this and experience the webpage becoming unresponsive or slow then try [configuring posthog](/docs/integrate/client/js#persistence) to use `persistence: "localStorage+cookie"`. This will use localStorage for the majority of storage needs, bypassing the infinite loop issue sometimes caused by cookie management tools.
|
||||
|
||||
### Recordings are not being captured
|
||||
|
||||
There are a few common reasons that you may not see recordings appear in your project.
|
||||
|
||||
#### Permitted domain setting
|
||||
|
||||
In your project settings page, there's a section for 'Authorized URLs'. This is the list of domains where posthog will capture recordings. You should make sure it's not too restrictive.
|
||||
|
||||
For example, you may have `https://www.example.com` in the list. This will stop PostHog from capturing recordings on `https://example.com`.
|
||||
|
||||
If no domains are set here, PostHog will capture recordings on all domains.
|
||||
|
||||
#### `posthog-js` configurations
|
||||
|
||||
If you had previously disabled session recordings, you may have set the `disable_session_recording` option to `true` in posthog-js.
|
||||
|
||||
To re-enable session recordings you want to either remove the `disable_session_recording` option or set it to `false`.
|
||||
|
||||
You can read more about [posthog-js configurations here](/docs/integrate/client/js#config).
|
||||
|
||||
#### Content security policy
|
||||
|
||||
When recordings are enabled, postHog-js will fetch a `recorder.js` script from the PostHog server. (This is not included in the default posthog-js to minimize the default bundle size)
|
||||
|
||||
Depending on your content security policy, this script may be blocked. If you have a `default-src`, `script-src`, `script-src-elem`, or `connect-src` directive in your CSP, you may need to add `https://app.posthog.com` (or your host URL if you are self hosted) to your list.
|
||||
|
||||
If PostHog is being blocked by your content security policy, you should see an error message in your developer console with more details.
|
||||
|
||||
#### Ad/tracking blockers
|
||||
|
||||
Some ad/tracking blockers will block PostHog from fetching `posthog-js`. If you're testing your app locally, you may need to disable any ad/tracking blockers that you're running in your browser.
|
||||
|
||||
### Website is not recording properly
|
||||
|
||||
If you're having issues with recordings not looking correct, there are a couple things to do.
|
||||
|
||||
#### Update posthog-js
|
||||
|
||||
We're always making improvements to the recordings feature, so you'll want to make sure that you're running the latest version of posthog-js on your website.
|
||||
|
||||
To check the version that you're using, you can run the following in your console:
|
||||
|
||||
```js
|
||||
window.posthog.LIB_VERSION
|
||||
```
|
||||
|
||||
#### Report your specific issue
|
||||
|
||||
To report a specific problem, you can open a [GitHub issue](https://github.com/PostHog/posthog/issues/new/choose). To help us figure it out as quickly as possible, please include the following information:
|
||||
|
||||
- The URL of the page that you're trying to record
|
||||
- The version of posthog that you're using
|
||||
- The version of posthog-js that you're using
|
||||
- Details about the specific issue with your recording (e.g. how it looks and how it should look)
|
||||
- If you're on posthog-cloud, a link to the specific recording
|
||||
- Any unique details about your website (e.g. the frameworks that you're using etc.)
|
||||
@@ -1,441 +0,0 @@
|
||||
---
|
||||
title: Feature Flags
|
||||
sidebar: Docs
|
||||
showTitle: true
|
||||
related:
|
||||
- ../tutorials/experiments.md
|
||||
- ../tutorials/metrics-tutorial.md
|
||||
- ../tutorials/cohorts.md
|
||||
availability:
|
||||
free: partial
|
||||
selfServe: full
|
||||
enterprise: full
|
||||
|
||||
features:
|
||||
multivariate:
|
||||
free: false
|
||||
selfServe: true
|
||||
enterprise: true
|
||||
---
|
||||
|
||||
Feature Flags enable you to safely deploy and roll back new features. This means you can ship the code for new features and roll it out to your users in a managed way. If something goes wrong, you can roll back without having to re-deploy your application.
|
||||
|
||||
Feature Flags also help you control access to certain parts of your product, such as only showing paid features to users with an active subscription.
|
||||
|
||||
> Looking to _test_ changes to your product? Check out our [Experimentation](/docs/user-guides/experimentation) feature.
|
||||
|
||||
## Learning resources
|
||||
|
||||
We have a number of comprehensive guides to using feature flags, including:
|
||||
|
||||
- [How to do a canary release with feature flags in PostHog](/tutorials/canary-release)
|
||||
- Tips for [feature flag best practices with examples](/blog/feature-flag-best-practices)
|
||||
|
||||
## Creating feature flags
|
||||
|
||||
In the PostHog app sidebar, go to 'Feature Flags' and click 'New feature flag'.
|
||||
|
||||
Think of a descriptive name and select how you want to roll out your feature.
|
||||
|
||||

|
||||
|
||||
## Persisting feature flags across authentication steps
|
||||
|
||||
You have an option to persist flags across authentication steps.
|
||||
|
||||
Consider this case: An anonymous person comes to your website and you use a flag to show them a green call to action.
|
||||
|
||||
Without persisting feature flags, the flag value can change on login because their identity can change (from anonymous to identified). Once they login, the flag might evaluate differently and show a red call to action instead.
|
||||
|
||||
This usually is not a problem since experiments run either completely for anonymous users, or completely for logged in users.
|
||||
|
||||
However, with some businesses, like e-commerce, it's very common to browse things anonymously and login right before checking out. In cases like these you can preserve the feature flag values by checking this checkbox.
|
||||
|
||||

|
||||
|
||||
Note that there are some performance trade-offs here. Specifically,
|
||||
|
||||
1. Enabling this slows down the feature flag response.
|
||||
2. It disables local evaluation of the feature flag.
|
||||
3. It disables bootstrapping this feature flag.
|
||||
|
||||
## Implementing the feature flag
|
||||
|
||||
When you create a feature flag, we'll show you an example snippet. It will look something like this:
|
||||
|
||||
<MultiLanguage>
|
||||
|
||||
```js
|
||||
if (posthog.isFeatureEnabled('new-beta-feature')) {
|
||||
// run your activation code here
|
||||
}
|
||||
```
|
||||
|
||||
```node
|
||||
const isMyFlagEnabledForUser = await client.isFeatureEnabled('new-beta-feature', 'user distinct id')
|
||||
|
||||
if (isMyFlagEnabledForUser) {
|
||||
// Do something differently for this user
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
if (PostHog::isFeatureEnabled('new-beta-feature', 'user distinct id')) {
|
||||
// do something here
|
||||
}
|
||||
```
|
||||
|
||||
```ruby
|
||||
is_my_flag_enabled = posthog.is_feature_enabled('new-beta-feature', 'user distinct id')
|
||||
|
||||
if is_my_flag_enabled
|
||||
# Do something differently for this user
|
||||
end
|
||||
```
|
||||
|
||||
```go
|
||||
isFlagEnabledForUser, err := client.IsFeatureEnabled(
|
||||
FeatureFlagPayload{
|
||||
Key: "new-beta-feature",
|
||||
DistinctId: "distinct-id",
|
||||
})
|
||||
|
||||
if (isFlagEnabledForUser) {
|
||||
// Do something differently for this user
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
if posthog.feature_enabled("new-beta-feature", "user_distinct_id"):
|
||||
runAwesomeFeature()
|
||||
```
|
||||
|
||||
```
|
||||
curl https://app.posthog.com/decide/ \
|
||||
-X POST -H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"api_key": "phc_Wg2jMXmDvsnrQ3IKuVyspZghFIVE2IbxuqVYlhm7ry0",
|
||||
"distinct_id": "[user distinct id]"
|
||||
}'
|
||||
```
|
||||
|
||||
</MultiLanguage>
|
||||
|
||||
What you do inside that if statement is up to you. You might change the CSS of a button, hide an entire section, or move elements around on the page.
|
||||
|
||||
### Ensuring flags are loaded before usage
|
||||
|
||||
Every time a user loads a page we send a request in the background to an endpoint to get the feature flags that apply to that user. In the client, we store those flags as a cookie.
|
||||
|
||||
This means that for most page views the feature flags will be available immediately, _except_ for the first time a user visits.
|
||||
|
||||
To combat that, there's a JavaScript callback you can use to wait for the flags to come in:
|
||||
|
||||
```js
|
||||
posthog.onFeatureFlags(function() {
|
||||
// feature flags are guaranteed to be available at this point
|
||||
if (posthog.isFeatureEnabled('new-beta-feature')) {
|
||||
// do something
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Bootstrapping flags
|
||||
|
||||
There is a delay between loading the library and feature flags becoming available to use. This can be detrimental if you want to do something like redirecting to a different page based on a feature flag.
|
||||
|
||||
To have your feature flags available immediately, you can bootstrap them with a distinct user ID and their values during initialization.
|
||||
|
||||
```js
|
||||
posthog.init('<ph_project_api_key>', {
|
||||
api_host: '<ph_instance_address>',
|
||||
bootstrap: {
|
||||
distinctID: 'your-anonymous-id',
|
||||
featureFlags: {
|
||||
'flag-1': true,
|
||||
'variant-flag': 'control',
|
||||
'other-flag': false
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
To get the flag values for bootstrapping, you can call `getAllFlags()` in your server-side library, then pass the values to your frontend initialization. If you don't do this, your bootstrap values might be different than the values PostHog provides.
|
||||
|
||||
If the distinct user ID is an identified ID (the value you called `posthog.identify()` with), you can also pass the `isIdentifiedID` option. This ensures this ID is treated as an identified ID in the library. This is helpful as it warns you when you try to do something wrong with this ID, like calling identify again.
|
||||
|
||||
```js
|
||||
posthog.init('<ph_project_api_key>', {
|
||||
api_host: '<ph_instance_address>',
|
||||
bootstrap: {
|
||||
distinctID: 'your-identified-id',
|
||||
isIdentifiedID: true,
|
||||
featureFlags: {
|
||||
'flag-1': true,
|
||||
'variant-flag': 'control',
|
||||
'other-flag': false
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Forcing feature flags to update
|
||||
|
||||
In our client-side JavaScript library, we store flags as a cookie to reduce the load on the server and improve the performance of your app. This prevents always needing to make an HTTP request, flag evaluation can simply refer to data stored locally in the browser. This is known as 'local evaluation.'
|
||||
|
||||
While this makes your app faster, it means if your user does something mid-session which causes the flag to turn on for them, this does not immediately update. As such, if you expect your app to have scenarios like this _and_ you want flags to update mid-session, you can reload them yourself, by using the `reloadFeatureFlags` function.
|
||||
|
||||
```js
|
||||
posthog.reloadFeatureFlags()
|
||||
```
|
||||
|
||||
Calling this function forces PostHog to hit the endpoint for the updated information, and ensures changes are reflected mid-session.
|
||||
|
||||
### Server-side local evaluation
|
||||
|
||||
If you're using our server-side libraries, you can use local evaluation to improve performance instead of making additional API requests. This requires:
|
||||
1. knowing and passing in all the person or group properties the flag relies on
|
||||
2. initializing the library with your personal API key (created in your account settings)
|
||||
|
||||
Local evaluation, in practice, looks like this:
|
||||
|
||||
<MultiLanguage>
|
||||
|
||||
```js
|
||||
await client.getFeatureFlag(
|
||||
'beta-feature',
|
||||
'distinct id',
|
||||
{
|
||||
personProperties: {'is_authorized': True}
|
||||
}
|
||||
)
|
||||
# returns string or None
|
||||
```
|
||||
|
||||
```python
|
||||
posthog.get_feature_flag(
|
||||
'beta-feature',
|
||||
'distinct id',
|
||||
person_properties={'is_authorized': True}
|
||||
)
|
||||
# returns string or None
|
||||
```
|
||||
|
||||
```php
|
||||
PostHog::getFeatureFlag(
|
||||
'beta-feature',
|
||||
'some distinct id',
|
||||
[],
|
||||
["is_authorized" => true]
|
||||
)
|
||||
// the third argument is for groups
|
||||
```
|
||||
|
||||
```ruby
|
||||
posthog.get_feature_flag(
|
||||
'beta-feature',
|
||||
'distinct id',
|
||||
person_properties: {'is_authorized': True}
|
||||
)
|
||||
# returns string or Nil
|
||||
```
|
||||
|
||||
```go
|
||||
enabledVariant, err := client.GetFeatureFlag(
|
||||
FeatureFlagPayload{
|
||||
Key: "multivariate-flag",
|
||||
DistinctId: "distinct-id",
|
||||
PersonProperties: posthog.NewProperties().
|
||||
Set("is_authorized", true),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
</MultiLanguage>
|
||||
|
||||
This works for `getAllFlags` as well. It evaluates all flags locally if possible, and if not, falls back to making a `decide` HTTP request.
|
||||
|
||||
<MultiLanguage>
|
||||
|
||||
```node
|
||||
await client.getAllFlags(
|
||||
'distinct id',
|
||||
{
|
||||
groups: {},
|
||||
personProperties: {'is_authorized': True},
|
||||
groupProperties: {}
|
||||
}
|
||||
)
|
||||
// returns dict of flag key and value pairs.
|
||||
```
|
||||
|
||||
```php
|
||||
PostHog::getAllFlags('distinct id', ["organisation" => "some-company"], [], ["organisation" => ["is_authorized" => true]])
|
||||
```
|
||||
|
||||
```go
|
||||
featureVariants, _ := client.GetAllFlags(FeatureFlagPayloadNoKey{
|
||||
DistinctId: "distinct-id",
|
||||
})
|
||||
```
|
||||
|
||||
```python
|
||||
posthog.get_all_flags('distinct id', groups={}, person_properties={'is_authorized': True}, group_properties={})
|
||||
# returns dict of flag key and value pairs.
|
||||
```
|
||||
|
||||
```ruby
|
||||
posthog.get_all_flags('distinct id', groups: {}, person_properties: {'is_authorized': True}, group_properties: {})
|
||||
# returns hash of flag key and value pairs.
|
||||
```
|
||||
|
||||
</MultiLanguage>
|
||||
|
||||
## Using locally
|
||||
|
||||
To test feature flags locally, you can open your developer tools and override the feature flags. You will get a warning that you're manually overriding feature flags.
|
||||
|
||||
```js
|
||||
posthog.feature_flags.override(['feature-flag-1', 'feature-flag-2'])
|
||||
```
|
||||
|
||||
This will persist until you call override again with the argument `false`:
|
||||
|
||||
```js
|
||||
posthog.feature_flags.override(false)
|
||||
```
|
||||
|
||||
To see the feature flags that are currently active for you, you can call:
|
||||
|
||||
```js
|
||||
posthog.feature_flags.getFlags()
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## Roll out the feature flag
|
||||
|
||||
There are three options for deciding who sees your new feature. You can roll out the feature to:
|
||||
|
||||
1. A fixed percentage of users or groups
|
||||
1. A set of users or groups filtered based on their user properties, cohort (based on user properties), or group properties.
|
||||
1. A combination of the two
|
||||
|
||||
### Roll out to a percentage of users or groups
|
||||
|
||||
By rolling out to a percentage of users or groups, you can gradually ramp up those who sees a new feature. To calculate this, we "hash" a combination of the key of the feature flag and the unique distinct ID of the user.
|
||||
|
||||
This way a user always falls in the same place between 0 and 100%, so they consistently see or do not see the feature controlled by the flag. As you move the slider towards 100%, more users start seeing your feature.
|
||||
|
||||
Hashing also means that the same user falls along different points of the line for each new feature. For example, a user may start seeing the feature at 5% for feature A, but only at 80% for feature B.
|
||||
|
||||
### Filter by user or group properties
|
||||
|
||||
This works just like any other filter in PostHog. You can select any property and users that match those filters will see your new feature.
|
||||
|
||||
By combining properties and percentages, you can determine something like:
|
||||
- Roll out this feature to 80% of users that have an email set
|
||||
- Provide access to this feature to 25% of organizations where the `beta-tester` property is `true`.
|
||||
- Show this component to 10% of users whose `signed_up_at` date is after January 1st.
|
||||
|
||||
## De-activating properties
|
||||
|
||||
If the feature has caused a problem, or you don't need the feature flag anymore, you can disable it instantly and completely. Doing so ensures _no users_ will have the flag enabled.
|
||||
|
||||
## Feature flag persistence
|
||||
|
||||
For feature flags that filter by user properties only, a given flag will always be on if a certain user meets all the specified property filters.
|
||||
|
||||
However, for flags using a rollout percentage mechanism (either by itself or in combination with user properties), the flag will persist for a given user as long as the rollout percentage and the flag key are not changed.
|
||||
|
||||
As a result, bear in mind that changing those values will result in flags being toggled on and off for certain users in a non-predictable way.
|
||||
|
||||
## Multivariate feature flags
|
||||
|
||||
<FeatureAvailability availability={_frontmatter.availability.features.multivariate} />
|
||||
|
||||
> Multivariate feature flags are only available when using PostHog >= 1.28 (if self-hosting) and [posthog-js](https://github.com/PostHog/posthog-js) >= 1.13.
|
||||
|
||||
PostHog 1.28 introduced support for multivariate feature flags which can return string values according to a specified distribution.
|
||||
|
||||
Some examples for a 3-variant case would be 33/33/34%, 50/25/25%, or 70/20/10%. This is ideal for when you want to test multiple variants of the same interchangeable content, such as marketing taglines, colors, or page layouts.
|
||||
|
||||
### Creating a feature flag with multiple variants
|
||||
|
||||
Create a multivariate feature flag just like you would a standard flag, and then change the "Served value" option to "a string value". You will then be prompted to enter a few keys with optional descriptions and set the distribution percentages for each.
|
||||
|
||||
Note that the rollout percentage of feature flag variants must add up to 100%. If you wish to exclude some users from your test, i.e. have some users receive no value at all, configure the _release condition groups_. While the release condition groups determine how many users will be bucketed into _any_ of the given variants, the rollout percentage of each variant determines the portion of the overall release group that will be assigned to that particular variant.
|
||||
|
||||
### Using multivariate feature flags in your code
|
||||
|
||||
With the latest version of our JS library, you can call:
|
||||
|
||||
```js
|
||||
if (posthog.getFeatureFlag('checkout-button-color') === 'black') {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
`getFeatureFlag` also returns true or false for standard (Boolean) feature flags, meaning that the following statements are equivalent:
|
||||
|
||||
```js
|
||||
posthog.isFeatureEnabled('new-beta-feature')
|
||||
posthog.getFeatureFlag('new-beta-feature') === true
|
||||
```
|
||||
|
||||
### `getFlagVariants`
|
||||
|
||||
Just as you can call `getFlags()` to return an array of feature flags that are currently active, you can call:
|
||||
|
||||
```js
|
||||
posthog.feature_flags.getFlagVariants()
|
||||
```
|
||||
|
||||
`getFlagVariants` returns an object:
|
||||
|
||||
```json
|
||||
{
|
||||
"new-beta-feature": true,
|
||||
"checkout-button-color": "black"
|
||||
}
|
||||
```
|
||||
|
||||
### `onFeatureFlags`
|
||||
|
||||
`onFeatureFlags(callback)` now passes the feature flag variants object as the second argument to `callback`, which looks like this:
|
||||
|
||||
```js
|
||||
posthog.onFeatureFlags(function (flags, flagVariants) {
|
||||
// do something useful
|
||||
console.log(flags) // ["new-beta-feature", "checkout-button-color"]
|
||||
console.log(flagVariants) // { "new-beta-feature": true, "checkout-button-color": "black" }
|
||||
})
|
||||
```
|
||||
|
||||
Note that `getFlags()` and the callback argument `flags` will include the key names of all truthy feature flags, including active multivariate feature flags.
|
||||
|
||||
### Querying data by multivariate feature flag values
|
||||
|
||||
With the latest version of our JS library, we send each feature flag's value as a separate property on every event. This means the values can be used in filters and breakdowns in Insights queries or wherever else you may choose to filter incoming events.
|
||||
|
||||
We send the event properties as `$feature/your-feature-name`, for example `$feature/checkout-button-color`. Standard (Boolean) flags are captured in this format as well.
|
||||
|
||||
For example, if you have a Trends graph of button click events and you'd like to narrow it down to clicks only when the checkout button is blue, apply a filter to your graph series such that `$feature/checkout-button-color = blue`.
|
||||
|
||||
If you'd like to compare all variants for which we have data in one graph, apply a breakdown by `$feature/checkout-button-color`.
|
||||
|
||||
## Experimentation
|
||||
|
||||
Feature Flags and Experimentation are different features and work for different use cases. Check out our [Experimentation](/docs/user-guides/experimentation) manual for more details.
|
||||
|
||||
### Further reading
|
||||
|
||||
Want to know more about what's possible with Feature Flags in PostHog? Try these tutorials:
|
||||
|
||||
- [How to do a canary release with feature flags](/tutorials/canary-release)
|
||||
- [Running experiments on new users](/tutorials/new-user-experiments)
|
||||
- [How to run Experiments without feature flags](/tutorials/experiments)
|
||||
|
||||
Using a library other than JavaScript for your feature flag implementation? Check out [these other libraries](/docs/integrate/libraries) for more details.
|
||||
|
||||
Want more? Check our [full list of PostHog tutorials](/tutorials).
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Support
|
||||
title: Support options
|
||||
sidebar: Docs
|
||||
showTitle: true
|
||||
---
|
||||
|
||||
@@ -40,7 +40,15 @@ export const wrapPageElement = ({ element, props }) => {
|
||||
return props.custom404 || !props.data ? (
|
||||
element
|
||||
) : /^handbook|^docs\/(?!api)|^manual/.test(slug) &&
|
||||
!['docs/api/post-only-endpoints', 'docs/api/user'].includes(slug) ? (
|
||||
![
|
||||
'docs/api/post-only-endpoints',
|
||||
'docs/api/user',
|
||||
'docs/product-analytics',
|
||||
'docs/session-recording',
|
||||
'docs/feature-flags',
|
||||
'docs/experiments',
|
||||
'docs/data',
|
||||
].includes(slug) ? (
|
||||
<HandbookLayout {...props} />
|
||||
) : /^product\//.test(slug) ? (
|
||||
<Product {...props} />
|
||||
|
||||
@@ -20,7 +20,15 @@ export const wrapPageElement = ({ element, props }) => {
|
||||
props.custom404 || !props.data ? (
|
||||
element
|
||||
) : /^handbook|^docs\/(?!api)|^manual/.test(slug) &&
|
||||
!['docs/api/post-only-endpoints', 'docs/api/user'].includes(slug) ? (
|
||||
![
|
||||
'docs/api/post-only-endpoints',
|
||||
'docs/api/user',
|
||||
'docs/product-analytics',
|
||||
'docs/session-recording',
|
||||
'docs/feature-flags',
|
||||
'docs/experiments',
|
||||
'docs/data',
|
||||
].includes(slug) ? (
|
||||
<HandbookLayout {...props} />
|
||||
) : /^product\//.test(slug) ? (
|
||||
<Product {...props} />
|
||||
|
||||
@@ -8,14 +8,13 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/PostHog/posthog.com"
|
||||
},
|
||||
"keywords": [
|
||||
"gatsby"
|
||||
],
|
||||
"keywords": ["gatsby"],
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "gatsby build",
|
||||
"start": "TAILWIND_MODE=watch gatsby develop -H 0.0.0.0 -p 8001",
|
||||
"format": "prettier --write \"**/*.{html,js,ts,tsx,json,yml,css,scss}\"",
|
||||
"clean": "gatsby clean",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"typegen": "kea-typegen write .",
|
||||
"update-sprite": "svg-sprite -s --symbol-dest src/components/productFeature/images/icons --symbol-sprite sprited-icons.svg src/components/productFeature/images/icons/*.svg",
|
||||
@@ -200,9 +199,7 @@
|
||||
"pre-commit": "node ./scripts/mdxImportGen && git add src/mdxGlobalComponents.js && lint-staged"
|
||||
}
|
||||
},
|
||||
"workspaces": [
|
||||
"plugins/*"
|
||||
],
|
||||
"workspaces": ["plugins/*"],
|
||||
"lint-staged": {
|
||||
"*.{html,js,ts,tsx,json,yml,css,scss}": "prettier --write",
|
||||
"*.{js,ts,tsx}": "eslint"
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import {
|
||||
Android,
|
||||
AWS,
|
||||
Azure,
|
||||
DigitalOcean,
|
||||
Docker,
|
||||
GCS,
|
||||
GitHub,
|
||||
HelmChart,
|
||||
Heroku,
|
||||
PostHog,
|
||||
Hobby,
|
||||
Ios,
|
||||
JS,
|
||||
More,
|
||||
NodeJS,
|
||||
ReactIcon,
|
||||
Ruby,
|
||||
Segment,
|
||||
Sentry,
|
||||
Shopify,
|
||||
Slack,
|
||||
WordPress,
|
||||
Zapier,
|
||||
} from 'components/Icons/Icons'
|
||||
import Link from 'components/Link'
|
||||
import usePostHog from '../../hooks/usePostHog'
|
||||
import React from 'react'
|
||||
|
||||
const icons = {
|
||||
android: Android,
|
||||
aws: AWS,
|
||||
azure: Azure,
|
||||
'digital ocean': DigitalOcean,
|
||||
docker: Docker,
|
||||
gcs: GCS,
|
||||
'helm chart': HelmChart,
|
||||
heroku: Heroku,
|
||||
ios: Ios,
|
||||
js: JS,
|
||||
nodejs: NodeJS,
|
||||
react: ReactIcon,
|
||||
ruby: Ruby,
|
||||
hobby: Hobby,
|
||||
shopify: Shopify,
|
||||
posthog: PostHog,
|
||||
segment: Segment,
|
||||
sentry: Sentry,
|
||||
wordpress: WordPress,
|
||||
zapier: Zapier,
|
||||
more: More,
|
||||
github: GitHub,
|
||||
slack: Slack,
|
||||
}
|
||||
|
||||
export default function DeployOption({ url, icon, title, disablePrefetch, badge }) {
|
||||
const posthog = usePostHog()
|
||||
const Icon = icon && icons[icon]
|
||||
const badgeClass = badge === 'new' ? 'success' : badge === 'beta' ? 'warning' : null
|
||||
return (
|
||||
<Link
|
||||
disablePrefetch={disablePrefetch}
|
||||
className="text-almost-black hover:text-almost-black dark:text-white dark:hover:text-white font-semibold p-2 hover:bg-gray-accent/40 active:hover:bg-gray-accent/60 dark:hover:bg-gray-accent/10 dark:active:bg-gray-accent/5 rounded flex items-center space-x-2 text-[14px]"
|
||||
to={url}
|
||||
onClick={() => {
|
||||
posthog?.capture('deploy option clicked', { deploy_option: title })
|
||||
}}
|
||||
>
|
||||
{Icon && <Icon className="w-6 h-6" />}
|
||||
<span>{title}</span>
|
||||
{badge && <span className={`lemon-tag ${badgeClass}`}>{badge}</span>}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
39
src/components/Docs/GettingStarted.tsx
Normal file
39
src/components/Docs/GettingStarted.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import { Analytics } from 'components/ProductIcons'
|
||||
import { CallToAction } from 'components/CallToAction'
|
||||
|
||||
type ProductAnalyticsProps = {
|
||||
articleType: string
|
||||
title: string
|
||||
description: string
|
||||
link: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const GettingStarted: React.FC<ProductAnalyticsProps> = ({
|
||||
articleType,
|
||||
title,
|
||||
description,
|
||||
link,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="clear-both relative flex items-center border-y border-dashed border-gray-accent-light dark:border-gray-accent-dark py-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4 w-full items-center">
|
||||
<div className="flex-1 w-full">
|
||||
<div className="flex flex-row items-center opacity-60 space-x-1 mb-2">
|
||||
<span className="text-xs uppercase font-semibold">{articleType}</span>
|
||||
</div>
|
||||
<h3 className="mb-2 mt-0">{title}</h3>
|
||||
<p className="max-w-md mb-0">{description}</p>
|
||||
</div>
|
||||
|
||||
<aside className="w-full sm:w-auto sm:flex-shrink">
|
||||
<CallToAction to={link}>Get started →</CallToAction>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block absolute right-0 top-0 max-w-1/2 h-full">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
src/components/Docs/LinkGrid.tsx
Normal file
36
src/components/Docs/LinkGrid.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import Link from 'components/Link'
|
||||
import * as ProductIcons from '../ProductIcons'
|
||||
|
||||
type LinkGridProps = {
|
||||
links: { name: string; to: string; description: string; icon?: string }[]
|
||||
}
|
||||
|
||||
export const LinkGrid: React.FC<LinkGridProps> = ({ links }) => {
|
||||
return (
|
||||
<ul className="p-0 m-0 grid md:grid-cols-2 gap-[1px]">
|
||||
{links.map((link) => {
|
||||
const Icon = link.icon && ProductIcons[link.icon]
|
||||
return (
|
||||
<li key={link.name} className="list-none">
|
||||
<Link
|
||||
to={link.to}
|
||||
key={link.name}
|
||||
disablePrefetch
|
||||
className="group px-2 py-2 rounded-sm flex items-center space-x-3 hover:bg-gray-accent-light dark:hover:bg-gray-accent-dark relative
|
||||
hover:scale-[1.01] hover:top-[-.5px] active:top-[0.25px] active:scale-[.99]"
|
||||
>
|
||||
{Icon && (
|
||||
<Icon className="w-10 h-10 p-2 text-primary dark:text-primary-dark bg-gray-accent-light group-hover:bg-gray-accent/50 dark:bg-gray-accent-dark grou-hover:bg-gray-accent/50 rounded-sm mt-1 lg:mt-0.5 shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-red m-0">{link.name}</h3>
|
||||
{/* {link.description} */}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
24
src/components/Docs/Tutorials.tsx
Normal file
24
src/components/Docs/Tutorials.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
|
||||
import Link from 'components/Link'
|
||||
import { Posts } from 'components/Blog'
|
||||
|
||||
type TutorialsProps = {
|
||||
tutorials: {
|
||||
edges: {
|
||||
node: any
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export const Tutorials: React.FC<TutorialsProps> = ({ tutorials }) => {
|
||||
return (
|
||||
<section className="my-12">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="m-0">Tutorials</h3>
|
||||
<Link to="/docs/tutorials">View all</Link>
|
||||
</div>
|
||||
<Posts posts={tutorials.edges} action={<Link to="/tutorials/all">View all</Link>} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { graphql, Link, useStaticQuery } from 'gatsby'
|
||||
import React from 'react'
|
||||
import Slugger from 'github-slugger'
|
||||
import { CallToAction } from 'components/CallToAction'
|
||||
import { GatsbyImage, getImage, ImageDataLike, StaticImage } from 'gatsby-plugin-image'
|
||||
|
||||
const query = graphql`
|
||||
query Chapters {
|
||||
@@ -13,6 +14,11 @@ const query = graphql`
|
||||
frontmatter {
|
||||
title
|
||||
description
|
||||
featuredImage {
|
||||
childImageSharp {
|
||||
gatsbyImageData(placeholder: NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
headings {
|
||||
depth
|
||||
@@ -49,13 +55,19 @@ type ChapterProps = {
|
||||
link: string
|
||||
}[]
|
||||
children?: React.ReactNode
|
||||
image?: ImageDataLike
|
||||
}
|
||||
|
||||
const Chapter: React.FC<ChapterProps> = ({ num, title, url, headings, children }) => {
|
||||
const Chapter: React.FC<ChapterProps> = ({ num, title, url, headings, children, image }) => {
|
||||
const gatsbyImage = image && getImage(image)
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-6 pb-6 mb-6">
|
||||
{/*<div className="w-full h-48 md:h-auto bg-gray-accent-light dark:bg-gray-accent-dark rounded"></div>*/}
|
||||
<div className="md:col-span-3 pt-2 pb-6 space-y-8">
|
||||
{gatsbyImage && (
|
||||
<div className="hidden sm:block max-w-[150px] aspect-square md:max-w-full h-auto bg-gray-accent-light dark:bg-gray-accent-dark rounded">
|
||||
<GatsbyImage alt={title} image={gatsbyImage} />
|
||||
</div>
|
||||
)}
|
||||
<div className="md:col-span-2 pt-2 pb-6 space-y-8">
|
||||
<div className="flex items-center justify-between border-b border-dashed border-gray-accent-light dark:border-gray-accent-dark pb-6">
|
||||
<div className="">
|
||||
<span className="text-black/20 dark:text-white/60 font-semibold">Chapter {num}</span>
|
||||
@@ -71,7 +83,7 @@ const Chapter: React.FC<ChapterProps> = ({ num, title, url, headings, children }
|
||||
<span className="block text-sm text-black/40 dark:text-white/60 font-semibold mt-6 !mb-3">
|
||||
Jump to:
|
||||
</span>
|
||||
<ol className="list-none !m-0 font-semibold space-y-4">
|
||||
<ol className="list-none !m-0 font-semibold space-y-4 pl-0">
|
||||
{headings.map((heading) => (
|
||||
<li key={heading.link} className="pl-6 jumpTo group flex items-center leading-none">
|
||||
<Link
|
||||
@@ -92,11 +104,12 @@ export const InstallChapter: React.FC = () => {
|
||||
const data = useStaticQuery(query)
|
||||
|
||||
const {
|
||||
frontmatter: { title, description },
|
||||
frontmatter: { title, description, featuredImage },
|
||||
} = data.allMdx.nodes.find((node: any) => node.fields.slug === '/docs/getting-started/install')
|
||||
|
||||
return (
|
||||
<Chapter
|
||||
image={featuredImage}
|
||||
num={1}
|
||||
title={title}
|
||||
url="/docs/getting-started/install"
|
||||
@@ -114,26 +127,63 @@ export const SendEventsChapter: React.FC = () => {
|
||||
const data = useStaticQuery(query)
|
||||
const node = data.allMdx.nodes.find((node: any) => node.fields.slug === '/docs/getting-started/send-events')
|
||||
const {
|
||||
frontmatter: { title, description },
|
||||
frontmatter: { title, description, featuredImage },
|
||||
headings,
|
||||
} = node
|
||||
|
||||
const filteredHeadings = filterHeadings(node.fields.slug, headings)
|
||||
|
||||
return <Chapter num={2} title={title} url={node.fields.slug} headings={filteredHeadings}></Chapter>
|
||||
return (
|
||||
<Chapter
|
||||
image={featuredImage}
|
||||
num={2}
|
||||
title={title}
|
||||
url={node.fields.slug}
|
||||
headings={filteredHeadings}
|
||||
></Chapter>
|
||||
)
|
||||
}
|
||||
|
||||
export const IdentifyUsersChapter: React.FC = () => {
|
||||
const data = useStaticQuery(query)
|
||||
const node = data.allMdx.nodes.find((node: any) => node.fields.slug === '/docs/getting-started/identify-users')
|
||||
const {
|
||||
frontmatter: { title, description },
|
||||
frontmatter: { title, description, featuredImage },
|
||||
headings,
|
||||
} = node
|
||||
|
||||
const filteredHeadings = filterHeadings(node.fields.slug, headings)
|
||||
|
||||
return <Chapter num={3} title={title} url={node.fields.slug} headings={filteredHeadings}></Chapter>
|
||||
return (
|
||||
<Chapter
|
||||
image={featuredImage}
|
||||
num={3}
|
||||
title={title}
|
||||
url={node.fields.slug}
|
||||
headings={filteredHeadings}
|
||||
></Chapter>
|
||||
)
|
||||
}
|
||||
|
||||
export const UserPropertiesChapter: React.FC = () => {
|
||||
const data = useStaticQuery(query)
|
||||
const node = data.allMdx.nodes.find((node: any) => node.fields.slug === '/docs/getting-started/user-properties')
|
||||
const {
|
||||
frontmatter: { title, description, featuredImage },
|
||||
headings,
|
||||
} = node
|
||||
|
||||
const filteredHeadings = filterHeadings(node.fields.slug, headings)
|
||||
|
||||
return (
|
||||
<Chapter
|
||||
image={featuredImage}
|
||||
num={4}
|
||||
title={title}
|
||||
url={node.fields.slug}
|
||||
headings={filteredHeadings}
|
||||
></Chapter>
|
||||
)
|
||||
}
|
||||
|
||||
export const ActionsAndInsightsChapter: React.FC = () => {
|
||||
@@ -142,27 +192,35 @@ export const ActionsAndInsightsChapter: React.FC = () => {
|
||||
(node: any) => node.fields.slug === '/docs/getting-started/actions-and-insights'
|
||||
)
|
||||
const {
|
||||
frontmatter: { title, description },
|
||||
headings,
|
||||
} = node
|
||||
|
||||
const filteredHeadings = filterHeadings(node.fields.slug, headings)
|
||||
|
||||
return <Chapter num={4} title={title} url={node.fields.slug} headings={filteredHeadings}></Chapter>
|
||||
}
|
||||
|
||||
export const GroupAnalyticsChapter: React.FC = () => {
|
||||
const data = useStaticQuery(query)
|
||||
const node = data.allMdx.nodes.find((node: any) => node.fields.slug === '/docs/getting-started/group-analytics')
|
||||
const {
|
||||
frontmatter: { title, description },
|
||||
frontmatter: { title, description, featuredImage },
|
||||
headings,
|
||||
} = node
|
||||
|
||||
const filteredHeadings = filterHeadings(node.fields.slug, headings)
|
||||
|
||||
return (
|
||||
<Chapter num={5} title={title} url={node.fields.slug} headings={filteredHeadings}>
|
||||
<Chapter
|
||||
image={featuredImage}
|
||||
num={5}
|
||||
title={title}
|
||||
url={node.fields.slug}
|
||||
headings={filteredHeadings}
|
||||
></Chapter>
|
||||
)
|
||||
}
|
||||
|
||||
export const GroupAnalyticsChapter: React.FC = () => {
|
||||
const data = useStaticQuery(query)
|
||||
const node = data.allMdx.nodes.find((node: any) => node.fields.slug === '/docs/getting-started/group-analytics')
|
||||
const {
|
||||
frontmatter: { title, description, featuredImage },
|
||||
headings,
|
||||
} = node
|
||||
|
||||
const filteredHeadings = filterHeadings(node.fields.slug, headings)
|
||||
|
||||
return (
|
||||
<Chapter image={featuredImage} num={6} title={title} url={node.fields.slug} headings={filteredHeadings}>
|
||||
<p>Identify users</p>
|
||||
</Chapter>
|
||||
)
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
import React from 'react'
|
||||
import { StaticImage } from 'gatsby-plugin-image'
|
||||
import { Link } from 'gatsby'
|
||||
import { quickLinks as productAnalyticsLinks } from '../../pages/docs/product-analytics'
|
||||
import { quickLinks as featureFlagsLinks } from '../../pages/docs/feature-flags'
|
||||
import { quickLinks as experimentsLinks } from '../../pages/docs/experiments'
|
||||
import { quickLinks as sessionRecordingLinks } from '../../pages/docs/session-recording'
|
||||
import { quickLinks as dataLinks } from '../../pages/docs/data'
|
||||
import { CallToAction } from 'components/CallToAction'
|
||||
|
||||
type NextStepProps = {
|
||||
title: string
|
||||
url?: string
|
||||
links: {
|
||||
title: string
|
||||
url: string
|
||||
name: string
|
||||
to: string
|
||||
}[]
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const NextStep: React.FC<NextStepProps> = ({ title, links, children }) => {
|
||||
export const NextStep: React.FC<NextStepProps> = ({ title, url, links, children }) => {
|
||||
return (
|
||||
<div className="space-y-6 pb-8">
|
||||
<div className="flex items-end justify-between border-b border-dashed border-gray-accent-light dark:border-gray-accent-dark">
|
||||
<h3>{title}</h3>
|
||||
<div className="w-36">{children}</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
<CallToAction href={url} type="secondary" size="sm">
|
||||
Visit section
|
||||
</CallToAction>
|
||||
</div>
|
||||
<div className="w-72">{children}</div>
|
||||
</div>
|
||||
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 list-none p-0 m-0">
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 list-none p-0 m-0 py-4 border-t border-dashed border-gray-accent-light dark:border-gray-accent-dark">
|
||||
{links.map((link) => (
|
||||
<li key={link.title} className="relative w-full py-2 flex items-center">
|
||||
<Link className="leading-none" to={link.url}>
|
||||
<span className="jumpTo pl-6">{link.title}</span>
|
||||
<li key={link.name} className="relative w-full py-2 flex items-center">
|
||||
<Link className="leading-none" to={link.to}>
|
||||
<span className="jumpTo pl-6">{link.name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
@@ -34,15 +46,7 @@ export const NextStep: React.FC<NextStepProps> = ({ title, links, children }) =>
|
||||
|
||||
export const ProductAnalytics = () => {
|
||||
return (
|
||||
<NextStep
|
||||
title="Product Analytics"
|
||||
links={[
|
||||
{ title: 'Deploy a reverse proxy', url: '/docs/integrate/proxy' },
|
||||
{ title: 'Ingest historical data', url: '/docs/integrate/ingest-historic-data' },
|
||||
{ title: 'Find your power users', url: '/tutorials/power-users' },
|
||||
{ title: '5 things to do after installing PostHog', url: '/tutorials/next-steps-after-installing' },
|
||||
]}
|
||||
>
|
||||
<NextStep title="Product Analytics" url="/docs/product-analytics" links={productAnalyticsLinks}>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
@@ -54,20 +58,23 @@ export const ProductAnalytics = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const SessionRecording = () => {
|
||||
return (
|
||||
<NextStep title="Session recording" url="/docs/session-recording" links={sessionRecordingLinks}>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
quality={100}
|
||||
className="w-full"
|
||||
src="../Home/Slider/images/session-recording-hog.png"
|
||||
/>
|
||||
</NextStep>
|
||||
)
|
||||
}
|
||||
|
||||
export const FeatureFlags = () => {
|
||||
return (
|
||||
<NextStep
|
||||
title="Feature flags"
|
||||
links={[
|
||||
{ title: 'Create your first feature flag', url: '/manual/feature-flags#creating-feature-flags' },
|
||||
{ title: 'Roll out a feature flag', url: '/manual/feature-flags#roll-out-the-feature-flag' },
|
||||
{ title: 'Set up a canary release', url: '/tutorials/canary-release' },
|
||||
{
|
||||
title: 'Feature flags with multiple variants',
|
||||
url: '/manual/feature-flags#multivariate-feature-flags',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NextStep title="Feature flags" url="/docs/feature-flags" links={featureFlagsLinks}>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
@@ -81,17 +88,7 @@ export const FeatureFlags = () => {
|
||||
|
||||
export const Experiments = () => {
|
||||
return (
|
||||
<NextStep
|
||||
title="Experiments"
|
||||
links={[
|
||||
{ title: 'Create your first experiment', url: '/manual/experimentation#how-to-use-experimentation' },
|
||||
{ title: 'Run an experiment on new users', url: '/tutorials/new-user-experiments' },
|
||||
{
|
||||
title: 'Experimentation under the hood',
|
||||
url: '/manual/experimentation#advanced-whats-under-the-hood',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NextStep title="A/B testing" url="/docs/experiments" links={experimentsLinks}>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
@@ -103,15 +100,30 @@ export const Experiments = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const Data = () => {
|
||||
return (
|
||||
<NextStep title="Data" url="/docs/data" links={dataLinks}>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
quality={100}
|
||||
className="w-full"
|
||||
src="../Product/images/hogs/data-warehouse.png"
|
||||
/>
|
||||
</NextStep>
|
||||
)
|
||||
}
|
||||
|
||||
export const Apps = () => {
|
||||
return (
|
||||
<NextStep
|
||||
title="Apps"
|
||||
url="/docs/apps"
|
||||
links={[
|
||||
{ title: 'Browse apps', url: '/apps' },
|
||||
{ title: 'Import Stripe data into PostHog', url: '/tutorials/stripe-payment-data' },
|
||||
{ title: 'Filter out unwanted events', url: '/tutorials/fewer-unwanted-events' },
|
||||
{ title: 'Build your own app', url: '/docs/apps/build' },
|
||||
{ name: 'Browse apps', to: '/apps' },
|
||||
{ name: 'Import Stripe data into PostHog', to: '/tutorials/stripe-payment-data' },
|
||||
{ name: 'Filter out unwanted events', to: '/tutorials/fewer-unwanted-events' },
|
||||
{ name: 'Build your own app', to: '/docs/apps/build' },
|
||||
]}
|
||||
>
|
||||
<StaticImage
|
||||
|
||||
@@ -440,6 +440,9 @@ ul li {
|
||||
blockquote *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.article-content blockquote *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
p *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ const gettingStarted: IFeature[] = [
|
||||
]
|
||||
|
||||
const products: IFeature[] = [
|
||||
{ title: 'Product analytics', icon: <Analytics />, url: '/using-posthog#product-analytics' },
|
||||
{ title: 'Session recording', icon: <SessionRecording />, url: '/manual/recordings' },
|
||||
{ title: 'Feature flags', icon: <FeatureFlags />, url: '/manual/feature-flags' },
|
||||
{ title: 'A/B testing', icon: <AbTesting />, url: '/manual/experimentation' },
|
||||
{ title: 'Platform & data', icon: <Projects />, url: '/using-posthog#data' },
|
||||
{ title: 'Product analytics', icon: <Analytics />, url: '/docs/product-analytics' },
|
||||
{ title: 'Session recording', icon: <SessionRecording />, url: '/docs/session-recording' },
|
||||
{ title: 'Feature flags', icon: <FeatureFlags />, url: '/docs/feature-flags' },
|
||||
{ title: 'A/B testing', icon: <AbTesting />, url: '/docs/experiments' },
|
||||
{ title: 'Platform & data', icon: <Projects />, url: '/docs/data' },
|
||||
]
|
||||
|
||||
const resources: IFeature[] = [
|
||||
|
||||
@@ -5,6 +5,8 @@ import React, { useEffect, useState } from 'react'
|
||||
import Link from 'components/Link'
|
||||
import { Link as ScrollLink } from 'react-scroll'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import * as ProductIcons from '../ProductIcons'
|
||||
import * as NotProductIcons from '../NotProductIcons'
|
||||
|
||||
const Chevron = ({ open, className = '' }: { open: boolean; className?: string }) => {
|
||||
return (
|
||||
@@ -31,6 +33,14 @@ const Chevron = ({ open, className = '' }: { open: boolean; className?: string }
|
||||
)
|
||||
}
|
||||
|
||||
const getIcon = (name: string) => {
|
||||
return ProductIcons[name]
|
||||
? ProductIcons[name]({ className: 'w-5' })
|
||||
: NotProductIcons[name]
|
||||
? NotProductIcons[name]({ className: 'w-5' })
|
||||
: null
|
||||
}
|
||||
|
||||
export default function Menu({
|
||||
name,
|
||||
url,
|
||||
@@ -119,8 +129,10 @@ export default function Menu({
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{icon ? (
|
||||
<span className="cursor-pointer flex items-center space-x-2 text-[17px] font-semibold text-black hover:text-black">
|
||||
<span className="w-[25px]">{icon}</span>
|
||||
<span className="cursor-pointer flex items-center space-x-2 font-semibold text-primary hover:text-primary dark:text-primary-dark dark:hover:text-primary-dark">
|
||||
<span className="w-[25px] opacity-70">
|
||||
{typeof icon === 'string' ? getIcon(icon) : icon}
|
||||
</span>
|
||||
<span>{name}</span>
|
||||
</span>
|
||||
) : (
|
||||
@@ -168,7 +180,11 @@ export default function Menu({
|
||||
</button>
|
||||
)}
|
||||
{isWithChild && (
|
||||
<motion.div initial={{ height: 0 }} animate={{ height: open ? 'auto' : 0 }}>
|
||||
<motion.div
|
||||
className={icon ? 'pl-[25px] -ml-2' : ''}
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: open ? 'auto' : 0 }}
|
||||
>
|
||||
{children.map((child) => {
|
||||
return <Menu handleLinkClick={handleLinkClick} key={child.name} {...child} />
|
||||
})}
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface IContributor {
|
||||
}
|
||||
|
||||
export interface IMenu {
|
||||
icon?: React.ReactNode
|
||||
icon?: string | React.ReactNode
|
||||
name: string
|
||||
url?: string
|
||||
children?: IMenu[]
|
||||
@@ -81,7 +81,7 @@ export interface IProps {
|
||||
mobileMenu?: boolean
|
||||
darkMode?: boolean
|
||||
fullWidthContent?: boolean
|
||||
setFullWidthContent: (fullWidth: boolean) => void
|
||||
setFullWidthContent?: (fullWidth: boolean) => void
|
||||
contentContainerClasses?: string
|
||||
stickySidebar?: boolean
|
||||
}
|
||||
|
||||
@@ -539,6 +539,27 @@ export const Retention = ({ className = '', ...other }: { className?: string }):
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
export const Sampling = ({ className = '', ...other }: { className?: string }): JSX.Element => (
|
||||
<svg
|
||||
width="100%"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className={`fill-current ${className}`}
|
||||
{...other}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9.544 10.101a5.306 5.306 0 0 1 4.68 7.673 5.313 5.313 0 0 1-7.12 2.39 5.305 5.305 0 0 1-2.383-7.116A5.314 5.314 0 0 1 9.544 10.1Zm.016-1.16a6.47 6.47 0 0 0-5.88 3.59 6.466 6.466 0 0 0 11 6.71l4.718 2.349c.55.273 1.212.05 1.485-.499a1.105 1.105 0 0 0-.498-1.485L15.67 17.26A6.468 6.468 0 0 0 9.56 8.94ZM5.827 5.44c-.338 0-.61.218-.61.676v1.24c0 .123.006.158.094.266l.14.175c.04.05.062.092.074.172l.178 1.104a.657.657 0 0 0 .098.278 7.23 7.23 0 0 1 1.346-.634l.12-.75a.322.322 0 0 1 .073-.171l.142-.175c.086-.108.092-.142.092-.267V6.115c0-.46-.272-.676-.61-.676H5.826ZM9.918 5.44c-.337 0-.61.218-.61.676v1.24c0 .123.007.158.094.266l.141.175c.04.05.06.092.073.172l.057.357c.53.015 1.064.09 1.59.227l.095-.585a.31.31 0 0 1 .073-.172l.142-.175c.087-.108.094-.142.094-.267V6.115c0-.46-.273-.676-.611-.676H9.918Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18.1 13.07c-.338 0-.611.218-.611.676v1.24c0 .123.005.158.093.266l.141.175a.31.31 0 0 1 .074.172l.177 1.105c.047.295.168.45.527.45h.334c.359 0 .479-.155.527-.45l.177-1.105a.327.327 0 0 1 .073-.172l.142-.175c.087-.108.092-.143.092-.266v-1.24c0-.459-.272-.676-.609-.676H18.1ZM14.009 5.44c-.34 0-.61.218-.61.676v1.24c0 .123.006.158.092.266l.142.175c.04.05.06.092.073.172l.177 1.104c.047.295.168.452.526.452h.336c.358 0 .478-.157.525-.452l.178-1.104a.317.317 0 0 1 .073-.172l.141-.175c.088-.108.095-.142.095-.267v-1.24c0-.458-.273-.675-.611-.675h-1.137ZM18.1 5.44c-.338 0-.611.218-.611.676v1.24c0 .123.005.158.093.266l.141.175a.31.31 0 0 1 .074.172l.177 1.104c.047.295.168.452.527.452h.334c.359 0 .479-.157.527-.452l.177-1.104a.324.324 0 0 1 .073-.172l.142-.175c.087-.108.092-.142.092-.267v-1.24c0-.458-.272-.675-.609-.675H18.1ZM7.18 3.65c.103.456-.163.912-.598 1.02-.433.109-.868-.173-.972-.628-.104-.454.164-.91.598-1.019.432-.108.868.172.972.627ZM11.271 3.65c.103.456-.164.912-.598 1.02-.434.109-.87-.173-.973-.628-.102-.454.165-.91.598-1.019.434-.108.87.172.973.627ZM15.362 3.65c.104.456-.164.912-.598 1.02-.433.109-.868-.173-.972-.628-.104-.454.162-.91.597-1.019.434-.108.87.172.973.627ZM19.453 3.65c.103.456-.164.912-.596 1.02-.434.109-.87-.173-.974-.628-.104-.454.165-.91.597-1.019.434-.108.87.172.973.627ZM19.453 11.28c.103.456-.164.912-.596 1.02-.434.108-.87-.172-.974-.627-.104-.454.165-.91.597-1.02.434-.108.87.173.973.628ZM6.452 13.706h1.851c.164 0 .293.045.385.13.092.086.157.222.157.442v2.088c0 .218-.03.287-.167.445-.054.06-.113.128-.16.186-.028.035-.025.083-.032.127l-.308 2.117c-.044.303-.616.314-.626.03l-.068-2.038c-.007-.201-.231-.196-.238 0l-.087 2.038c-.013.284-.542.273-.585-.03l-.313-2.173a.188.188 0 0 0-.068-.12l-.12-.138c-.147-.169-.165-.245-.165-.452v-2.08c0-.22.065-.356.156-.442a.554.554 0 0 1 .388-.13ZM8.183 12.187a.848.848 0 0 1-.614 1.022.83.83 0 0 1-1-.629.848.848 0 0 1 .613-1.02.83.83 0 0 1 1 .627ZM10.635 13.706h1.852a.55.55 0 0 1 .385.13c.092.086.157.222.157.442v2.088c0 .218-.03.287-.167.445-.054.06-.113.128-.16.186-.028.035-.025.083-.031.127l-.308 2.117c-.043.303-.616.314-.625.03l-.07-2.038c-.006-.201-.23-.196-.237 0l-.086 2.038c-.013.284-.542.273-.587-.03l-.313-2.173a.18.18 0 0 0-.067-.12l-.12-.138c-.147-.169-.166-.245-.166-.452v-2.08c0-.22.066-.356.157-.442a.554.554 0 0 1 .386-.13ZM12.367 12.187a.848.848 0 0 1-.614 1.022.829.829 0 0 1-1-.629.847.847 0 0 1 .614-1.02.828.828 0 0 1 1 .627Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
export const SelfHosting = ({ className = '', ...other }: { className?: string }): JSX.Element => (
|
||||
<svg
|
||||
width="100%"
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { TrackedCTA } from 'components/CallToAction/index.tsx'
|
||||
import DeployOption from 'components/DeployOption'
|
||||
import { Slack } from 'components/Icons/Icons'
|
||||
import { Plan } from 'components/Pricing/PricingTable/Plan'
|
||||
import React from 'react'
|
||||
|
||||
export default function DeploymentOptions({ className = '' }) {
|
||||
return (
|
||||
<Plan
|
||||
title="Deploy to your infrastructure"
|
||||
subtitle="Host your own instance of PostHog anywhere in the world."
|
||||
className={className}
|
||||
>
|
||||
<ul className="list-none p-0 grid grid-cols-2 gap-1 my-7">
|
||||
<DeployOption title="Amazon AWS" icon="aws" url="/docs/self-host/deploy/aws" />
|
||||
<DeployOption title="Google Cloud" icon="gcs" url="/docs/self-host/deploy/gcp" />
|
||||
<DeployOption title="Helm Chart" icon="helm chart" url="/docs/self-host/deploy/other" />
|
||||
<DeployOption title="DigitalOcean" icon="digital ocean" url="/docs/self-host/deploy/digital-ocean" />
|
||||
<DeployOption title="Source" icon="github" url="https://github.com/PostHog/posthog" />
|
||||
</ul>
|
||||
<div className="flex justify-between items-center bg-gray-accent-light px-[18px] py-[16px] rounded-md flex-col xl:flex-row space-y-2 xl:space-y-0 ">
|
||||
<p className="m-0 font-bold">Deployment questions?</p>
|
||||
<TrackedCTA
|
||||
size="md"
|
||||
type="outline"
|
||||
className="bg-white flex space-x-2 items-center font-bold"
|
||||
to="/slack"
|
||||
event={{ name: 'deploy: clicked Join Slack' }}
|
||||
>
|
||||
<Slack className="w-4 h-4" />
|
||||
<span>Join our Slack</span>
|
||||
</TrackedCTA>
|
||||
</div>
|
||||
</Plan>
|
||||
)
|
||||
}
|
||||
@@ -480,7 +480,7 @@ const ProfileSidebar = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const query = graphql`
|
||||
const query = graphql`
|
||||
{
|
||||
issues: allPostHogIssue {
|
||||
nodes {
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
import React from 'react'
|
||||
import Layout from 'components/Layout'
|
||||
import { SEO } from 'components/seo'
|
||||
import Icon from 'components/SupportImages/Icon'
|
||||
import DeployOption from 'components/DeployOption'
|
||||
import { StaticImage } from 'gatsby-plugin-image'
|
||||
import Link from 'components/Link'
|
||||
import PostLayout from 'components/PostLayout'
|
||||
import docs from 'sidebars/docs.json'
|
||||
|
||||
const quickLinks = [
|
||||
{
|
||||
icon: 'selfHost',
|
||||
name: 'Deploy',
|
||||
to: '/docs/self-host',
|
||||
description: 'Detailed information on getting PostHog running.',
|
||||
},
|
||||
{
|
||||
icon: 'api',
|
||||
name: 'Integrate',
|
||||
to: '/docs/integrate',
|
||||
description: 'Connect PostHog to your website, app, or backend.',
|
||||
},
|
||||
{
|
||||
icon: 'apps',
|
||||
name: 'Apps',
|
||||
to: '/docs/apps',
|
||||
description: 'Extend PostHog by adding your own functionality.',
|
||||
},
|
||||
{
|
||||
icon: 'partners',
|
||||
name: 'Tutorials',
|
||||
to: '/tutorials',
|
||||
description: 'Step-by-step guides on how to use every feature.',
|
||||
},
|
||||
{
|
||||
icon: 'feature-flags',
|
||||
name: 'API',
|
||||
to: '/docs/api',
|
||||
description: 'Interact with PostHog programmatically.',
|
||||
},
|
||||
{
|
||||
icon: 'faq2',
|
||||
name: 'Ask a question',
|
||||
to: '/questions',
|
||||
description: "Can't find what you're looking for? Ask a question here.",
|
||||
},
|
||||
]
|
||||
|
||||
const deployment = [
|
||||
{ name: 'PostHog Cloud', to: '/signup', icon: 'posthog', badge: undefined },
|
||||
{ name: 'AWS', to: '/docs/self-host/deploy/aws', icon: 'aws', badge: undefined },
|
||||
{ name: 'Google Cloud', to: '/docs/self-host/deploy/gcp', icon: 'gcs', badge: undefined },
|
||||
{ name: 'DigitalOcean', to: '/docs/self-host/deploy/digital-ocean', icon: 'digital ocean', badge: undefined },
|
||||
{ name: 'Azure', to: '/docs/self-host/deploy/azure', icon: 'azure', badge: 'beta' },
|
||||
{ name: 'Hobby', to: '/docs/self-host/deploy/hobby', icon: 'docker', badge: undefined },
|
||||
]
|
||||
|
||||
const libraries = [
|
||||
{ name: 'JavaScript', to: '/docs/integrate/client/js', icon: 'js' },
|
||||
{ name: 'NodeJS', to: '/docs/integrate/server/node', icon: 'nodejs' },
|
||||
{ name: 'Ruby', to: '/docs/integrate/server/ruby', icon: 'ruby' },
|
||||
{ name: 'React Native', to: '/docs/integrate/client/react-native', icon: 'react' },
|
||||
{ name: 'iOS', to: '/docs/integrate/client/ios', icon: 'ios' },
|
||||
{ name: 'Android', to: '/docs/integrate/client/android', icon: 'android' },
|
||||
]
|
||||
|
||||
const apps = [
|
||||
{ name: 'Segment', to: '/docs/integrate/third-party/segment', icon: 'segment' },
|
||||
{ name: 'Sentry', to: '/docs/integrate/third-party/sentry', icon: 'sentry' },
|
||||
{ name: 'Slack', to: '/docs/integrate/webhooks/slack', icon: 'slack' },
|
||||
{ name: 'Shopify', to: '/docs/integrate/third-party/shopify', icon: 'shopify' },
|
||||
{ name: 'WordPress', to: '/docs/integrate/third-party/wordpress', icon: 'wordpress' },
|
||||
{ name: 'Zapier', to: '/apps/zapier-connector', icon: 'zapier' },
|
||||
]
|
||||
|
||||
const otherLinks = [
|
||||
{
|
||||
name: 'Integrate PostHog',
|
||||
links: [
|
||||
{ name: 'Live events', to: '/docs/integrate/ingest-live-data' },
|
||||
{ name: 'Historical events', to: '/docs/integrate/ingest-historic-data' },
|
||||
{ name: 'Identifying users', to: '/docs/integrate/identifying-users' },
|
||||
{ name: 'Libraries', to: '/docs/integrate/libraries' },
|
||||
{ name: 'Proxying events', to: '/docs/integrate/proxy' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Product manual',
|
||||
links: [
|
||||
{ name: 'Trends', to: '/manual/trends' },
|
||||
{ name: 'Funnels', to: '/manual/funnels' },
|
||||
{ name: 'Retention', to: '/manual/retention' },
|
||||
{ name: 'Session recording', to: '/manual/recordings' },
|
||||
{ name: 'Feature flags', to: '/manual/feature-flags' },
|
||||
{ name: 'Experimentation', to: '/manual/experimentation' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Self-host',
|
||||
links: [
|
||||
{ name: 'Deployment', to: '/docs/self-host' },
|
||||
{ name: 'Runbook', to: '/docs/runbook' },
|
||||
{ name: 'Environment variables', to: '/docs/self-host/configure/environment-variables' },
|
||||
{ name: 'Monitoring', to: '/docs/self-host/configure/monitoring-with-grafana' },
|
||||
{ name: 'Upgrading', to: '/docs/runbook/upgrading-posthog' },
|
||||
{ name: 'Troubleshooting', to: '/docs/self-host/deploy/troubleshooting' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Apps',
|
||||
links: [
|
||||
{ name: 'Explore app library', to: '/apps' },
|
||||
{ name: 'Use cases', to: '/docs/apps' },
|
||||
{ name: 'Building an app', to: '/docs/apps/build' },
|
||||
{ name: 'Developer reference', to: '/docs/apps/build/reference' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Developers',
|
||||
links: [
|
||||
{ name: 'REST API', to: '/docs/api' },
|
||||
{ name: 'Developing locally', to: '/handbook/engineering/developing-locally' },
|
||||
{ name: 'Contributing', to: '/docs/contribute' },
|
||||
{ name: 'How PostHog works', to: '/docs/how-posthog-works' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Privacy & compliance',
|
||||
links: [
|
||||
{ name: 'GDPR', to: '/docs/privacy/gdpr-compliance' },
|
||||
{ name: 'HIPAA', to: '/docs/privacy/hipaa-compliance' },
|
||||
{ name: 'CCPA', to: '/docs/privacy/ccpa-compliance' },
|
||||
{ name: 'Data deletion', to: '/docs/privacy/data-deletion' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const DocsIndex: React.FC = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Documentation - PostHog" />
|
||||
|
||||
<PostLayout article={false} title={'Docs'} menu={docs} hideSidebar hideSurvey>
|
||||
<div className="space-y-16 lg:space-y-20 lg:-mt-12 mb-8">
|
||||
<section>
|
||||
<div className="flex justify-start relative pb-12 md:pt-12 md:pb-20 lg:pt-16 lg:pb-16 items-center -mx-px h-80">
|
||||
<div className="w-full z-20">
|
||||
<h1 className="font-bold text-5xl mb-2">Documentation</h1>
|
||||
<h5 className="opacity-60 font-semibold leading-tight mb-8">
|
||||
In-depth tutorials, references, and <br className="hidden md:block xl:hidden" />
|
||||
examples for everything PostHog
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="absolute hidden md:block overflow-hidden inset-y-0 right-0 h-full w-full z-10">
|
||||
<span className="absolute right-0 bottom-0">
|
||||
<StaticImage
|
||||
src="../../contents/images/search-hog-3.png"
|
||||
alt="This hog has an answer"
|
||||
width={400}
|
||||
placeholder="blurred"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 border-l border-t border-dashed border-gray-accent-light dark:border-gray-accent-dark">
|
||||
{quickLinks.map((link) => {
|
||||
return (
|
||||
<Link
|
||||
to={link.to}
|
||||
key={link.name}
|
||||
disablePrefetch
|
||||
className="border-b border-r border-dashed border-gray-accent-light dark:border-gray-accent-dark px-8 py-4 flex items-start space-x-3 hover:bg-gray-accent-light dark:hover:bg-gray-accent-dark"
|
||||
>
|
||||
<Icon className="w-6 h-6 text-gray mt-1 lg:mt-0.5 shrink-0" name={link.icon} />
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-red mb-0.5">{link.name}</h3>
|
||||
<p className="text-black dark:text-white font-medium mb-2 text-gray-accent-dark text-sm">
|
||||
{link.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-8">
|
||||
<div className="text-center">
|
||||
<h2 className="font-bold mb-1">Get started</h2>
|
||||
<p className="text-gray font-medium">Information on how to get PostHog up and running</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-x-4 rounded xl:rounded-none overflow-hidden">
|
||||
<div className="bg-gray-accent-light dark:bg-gray-accent-dark lg:rounded px-6 py-4">
|
||||
<div>
|
||||
<h4 className="font-bold mb-0">
|
||||
<span className="text-gray text-lg">1.</span> Deploy
|
||||
</h4>
|
||||
<p className="text-base text-gray">Spin up your PostHog instance</p>
|
||||
</div>
|
||||
|
||||
<ul className="grid grid-cols-2 xl:grid-cols-1 w-full list-none m-0 p-0 space-y-1">
|
||||
{deployment.map((deploy) => {
|
||||
return (
|
||||
<li className="flex-grow" key={deploy.name}>
|
||||
<DeployOption
|
||||
url={deploy.to}
|
||||
title={deploy.name}
|
||||
icon={deploy.icon}
|
||||
disablePrefetch
|
||||
badge={deploy.badge}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-accent-light dark:bg-gray-accent-dark xl:rounded px-6 py-4">
|
||||
<div>
|
||||
<h4 className="font-bold mb-0">
|
||||
<span className="text-gray text-lg">2.</span> Integrate
|
||||
</h4>
|
||||
<p className="text-base text-gray">Start tracking events and users</p>
|
||||
</div>
|
||||
<ul className="grid grid-cols-2 xl:grid-cols-1 w-full list-none m-0 p-0 space-y-1">
|
||||
{libraries.map((library) => {
|
||||
return (
|
||||
<li className="flex-grow" key={library.name}>
|
||||
<DeployOption
|
||||
url={library.to}
|
||||
title={library.name}
|
||||
icon={library.icon}
|
||||
disablePrefetch
|
||||
badge={undefined}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-accent-light dark:bg-gray-accent-dark xl:rounded px-6 py-4">
|
||||
<h4 className="font-bold mb-0">
|
||||
<span className="text-gray text-lg">3.</span> Customize
|
||||
</h4>
|
||||
<p className="text-base text-gray">Customize your installation</p>
|
||||
<ul className="grid grid-cols-2 xl:grid-cols-1 w-full list-none m-0 p-0 space-y-1">
|
||||
{apps.map((app) => {
|
||||
return (
|
||||
<li className="flex-grow" key={app.name}>
|
||||
<DeployOption
|
||||
url={app.to}
|
||||
title={app.name}
|
||||
icon={app.icon}
|
||||
disablePrefetch
|
||||
badge={undefined}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-8">
|
||||
<div className="text-center">
|
||||
<h2 className="font-bold mb-1">Important links</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 border-t border-l border-dashed border-gray-accent-light dark:border-gray-accent-dark">
|
||||
{otherLinks.map((category) => {
|
||||
return (
|
||||
<div
|
||||
key={category.name}
|
||||
className="space-y-2 py-4 md:py-6 px-4 md:px-8 border-dashed border-b border-r border-gray-accent-light dark:border-gray-accent-dark"
|
||||
>
|
||||
<h4 className="mb-0">{category.name}</h4>
|
||||
<ul className="p-0 space-y-1">
|
||||
{category.links.map((link) => {
|
||||
return (
|
||||
<li key={link.to} className="list-none">
|
||||
<Link to={link.to}>{link.name}</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PostLayout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocsIndex
|
||||
136
src/pages/docs/data.tsx
Normal file
136
src/pages/docs/data.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import { StaticImage } from 'gatsby-plugin-image'
|
||||
|
||||
import docs from 'sidebars/docs.json'
|
||||
import Layout from 'components/Layout'
|
||||
import { SEO } from 'components/seo'
|
||||
import PostLayout from 'components/PostLayout'
|
||||
import { Tutorials } from 'components/Docs/Tutorials'
|
||||
import { LinkGrid } from 'components/Docs/LinkGrid'
|
||||
import { GettingStarted } from 'components/Docs/GettingStarted'
|
||||
|
||||
export const quickLinks = [
|
||||
{
|
||||
name: 'Actions',
|
||||
to: '/manual/actions',
|
||||
description: 'Combine and filter events to create custom actions.',
|
||||
},
|
||||
{
|
||||
name: 'Cohorts',
|
||||
to: '/manual/cohorts',
|
||||
description: 'Create groups of users based on their behavior or properties.',
|
||||
},
|
||||
{
|
||||
name: 'Events',
|
||||
to: '/manual/events',
|
||||
description: 'Core information on events and event properties.',
|
||||
},
|
||||
{
|
||||
name: 'Persons',
|
||||
to: '/manual/persons',
|
||||
description: 'Identify your users and their properties.',
|
||||
},
|
||||
{
|
||||
name: 'Organizations & projects',
|
||||
to: '/manual/organizations-and-projects',
|
||||
description: 'Organize your data into projects and manage access to them.',
|
||||
},
|
||||
{
|
||||
name: 'Notifications & alerts',
|
||||
to: '/manual/notification-and-alerts',
|
||||
description: 'Set up notifications for when specific actions occur',
|
||||
},
|
||||
]
|
||||
|
||||
type DataProps = {
|
||||
data: {
|
||||
tutorials: {
|
||||
edges: {
|
||||
node: any
|
||||
}[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Data: React.FC<DataProps> = ({ data }) => {
|
||||
const { tutorials } = data
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Data - Docs - PostHog" />
|
||||
|
||||
<PostLayout title={'Data'} menu={docs} hideSurvey hideSidebar>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
quality={100}
|
||||
className="w-full sm:w-[400px] sm:float-right sm:ml-8 sm:-mt-8 sm:mb-8"
|
||||
src="../../components/Product/images/hogs/data-warehouse.png"
|
||||
/>
|
||||
<h1 className="text-4xl mb-2 mt-6">Data management</h1>
|
||||
<h3 className="text-lg font-semibold text-primary/60 dark:text-primary-dark/75">
|
||||
Manage event and customer data used throughout the PostHog suite.
|
||||
</h3>
|
||||
|
||||
{/* Get started section */}
|
||||
<section className="py-6 sm:py-12">
|
||||
<GettingStarted
|
||||
product="Data management"
|
||||
title="Data management primer"
|
||||
description="Learn how to manage event data in PostHog."
|
||||
link="/manual/data-management"
|
||||
></GettingStarted>
|
||||
</section>
|
||||
|
||||
{/* Quick links */}
|
||||
<section className="my-12">
|
||||
<h3 className="mb-6 mt-0">Quick links</h3>
|
||||
<LinkGrid links={quickLinks} />
|
||||
</section>
|
||||
|
||||
<Tutorials tutorials={tutorials} />
|
||||
</PostLayout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Data
|
||||
|
||||
export const query = graphql`
|
||||
query Data {
|
||||
tutorials: allMdx(
|
||||
limit: 6
|
||||
sort: { order: DESC, fields: [frontmatter___date] }
|
||||
filter: { frontmatter: { tags: { in: ["data management"] } }, fields: { slug: { regex: "/^/tutorials/" } } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
date(formatString: "MMM 'YY")
|
||||
Category: tags
|
||||
Contributor: authorData {
|
||||
id
|
||||
image {
|
||||
childImageSharp {
|
||||
gatsbyImageData(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
name
|
||||
}
|
||||
featuredImage {
|
||||
childImageSharp {
|
||||
gatsbyImageData(placeholder: NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
122
src/pages/docs/experiments.tsx
Normal file
122
src/pages/docs/experiments.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import { StaticImage } from 'gatsby-plugin-image'
|
||||
|
||||
import docs from 'sidebars/docs.json'
|
||||
import Layout from 'components/Layout'
|
||||
import { SEO } from 'components/seo'
|
||||
import PostLayout from 'components/PostLayout'
|
||||
import { Tutorials } from 'components/Docs/Tutorials'
|
||||
import { LinkGrid } from 'components/Docs/LinkGrid'
|
||||
import { GettingStarted } from 'components/Docs/GettingStarted'
|
||||
|
||||
export const quickLinks = [
|
||||
{
|
||||
name: 'Creating an experiment',
|
||||
to: '/docs/experiments/manual#creating-an-experiment',
|
||||
description: 'Create an experiment to test a hypothesis.',
|
||||
},
|
||||
{
|
||||
name: 'Statistical significance',
|
||||
to: '/docs/experiments/significance',
|
||||
description: 'Notes on how to interpret statistical significance.',
|
||||
},
|
||||
{
|
||||
name: 'Under the hood',
|
||||
to: '/docs/experiments/under-the-hood',
|
||||
description: 'Detailed information on how experiments work',
|
||||
},
|
||||
]
|
||||
|
||||
type ExperimentsProps = {
|
||||
data: {
|
||||
tutorials: {
|
||||
edges: {
|
||||
node: any
|
||||
}[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Experiments: React.FC<ExperimentsProps> = ({ data }) => {
|
||||
const { tutorials } = data
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Experiments - Docs - PostHog" />
|
||||
|
||||
<PostLayout title={'Experiments'} menu={docs} hideSurvey hideSidebar>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
quality={100}
|
||||
className="w-full sm:w-[400px] sm:float-right sm:ml-8 sm:-mt-8 sm:mb-8"
|
||||
src="../../components/Home/Slider/images/ab-testing-hog.png"
|
||||
/>
|
||||
<h1 className="text-4xl mb-2 mt-6">Experiments</h1>
|
||||
<h3 className="text-lg font-semibold text-primary/60 dark:text-primary-dark/75 leading-tighttext-lg text-gray">
|
||||
Test changes in production with an experimentation suite that makes it easy to get the results you
|
||||
want.
|
||||
</h3>
|
||||
|
||||
{/* Get started section */}
|
||||
<section className="py-6 sm:py-12">
|
||||
<GettingStarted
|
||||
product="Experiments"
|
||||
title="Roll out your first feature"
|
||||
description="Start A/B testing your features in minutes."
|
||||
link="/docs/experiments/manual#creating-an-experiment"
|
||||
></GettingStarted>
|
||||
</section>
|
||||
|
||||
{/* Quick links */}
|
||||
<section className="my-6">
|
||||
<h3 className="mb-6 mt-0">Quick links</h3>
|
||||
<LinkGrid links={quickLinks} />
|
||||
</section>
|
||||
|
||||
<Tutorials tutorials={tutorials} />
|
||||
</PostLayout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Experiments
|
||||
|
||||
export const query = graphql`
|
||||
query Experiments {
|
||||
tutorials: allMdx(
|
||||
limit: 6
|
||||
sort: { order: DESC, fields: [frontmatter___date] }
|
||||
filter: { frontmatter: { tags: { in: ["experimentation"] } }, fields: { slug: { regex: "/^/tutorials/" } } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
date(formatString: "MMM 'YY")
|
||||
Category: tags
|
||||
Contributor: authorData {
|
||||
id
|
||||
image {
|
||||
childImageSharp {
|
||||
gatsbyImageData(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
name
|
||||
}
|
||||
featuredImage {
|
||||
childImageSharp {
|
||||
gatsbyImageData(placeholder: NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
121
src/pages/docs/feature-flags.tsx
Normal file
121
src/pages/docs/feature-flags.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import { StaticImage } from 'gatsby-plugin-image'
|
||||
|
||||
import docs from 'sidebars/docs.json'
|
||||
import Layout from 'components/Layout'
|
||||
import { SEO } from 'components/seo'
|
||||
import PostLayout from 'components/PostLayout'
|
||||
import { Tutorials } from 'components/Docs/Tutorials'
|
||||
import { LinkGrid } from 'components/Docs/LinkGrid'
|
||||
import { GettingStarted } from 'components/Docs/GettingStarted'
|
||||
|
||||
export const quickLinks = [
|
||||
{
|
||||
name: 'Local evaluation',
|
||||
to: '/docs/feature-flags/local-evaluation',
|
||||
description: 'Evaluate flags locally when you need an immediate response.',
|
||||
},
|
||||
{
|
||||
name: 'Rollout strategies',
|
||||
to: '/docs/feature-flags/rollout-strategies',
|
||||
description: 'Control how your feature flags are rolled out to your users.',
|
||||
},
|
||||
{
|
||||
name: 'Multivariate flags',
|
||||
to: '/docs/feature-flags/multivariate-flags',
|
||||
description: 'Test features with multiple variants.',
|
||||
},
|
||||
]
|
||||
|
||||
type FeatureFlagsProps = {
|
||||
data: {
|
||||
tutorials: {
|
||||
edges: {
|
||||
node: any
|
||||
}[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FeatureFlags: React.FC<FeatureFlagsProps> = ({ data }) => {
|
||||
const { tutorials } = data
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Feature flags - Docs - PostHog" />
|
||||
|
||||
<PostLayout title={'Feature flags'} menu={docs} hideSurvey hideSidebar>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
quality={100}
|
||||
className="w-full sm:w-[400px] sm:float-right sm:ml-8 sm:-mt-8 sm:mb-8"
|
||||
src="../../components/Home/Slider/images/feature-flags-hog.png"
|
||||
/>
|
||||
<h1 className="text-4xl mb-2 mt-6">Feature flags</h1>
|
||||
<h3 className="text-lg font-semibold text-primary/60 dark:text-primary-dark/75 leading-tight">
|
||||
Toggle features for cohorts or individuals to test the impact before rolling out to everyone.
|
||||
</h3>
|
||||
|
||||
{/* Get started section */}
|
||||
<section className="py-6 sm:py-12">
|
||||
<GettingStarted
|
||||
product="Feature flags"
|
||||
title="Create your first feature flag"
|
||||
description="Learn how to create a feature flag and toggle it on and off for different users."
|
||||
link="/docs/feature-flags/manual#creating-feature-flags"
|
||||
></GettingStarted>
|
||||
</section>
|
||||
|
||||
{/* Quick links */}
|
||||
<section className="my-6">
|
||||
<h3 className="mb-6 mt-0">Quick links</h3>
|
||||
<LinkGrid links={quickLinks} />
|
||||
</section>
|
||||
|
||||
<Tutorials tutorials={tutorials} />
|
||||
</PostLayout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeatureFlags
|
||||
|
||||
export const query = graphql`
|
||||
query FeatureFlags {
|
||||
tutorials: allMdx(
|
||||
limit: 6
|
||||
sort: { order: DESC, fields: [frontmatter___date] }
|
||||
filter: { frontmatter: { tags: { in: ["feature flags"] } }, fields: { slug: { regex: "/^/tutorials/" } } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
date(formatString: "MMM 'YY")
|
||||
Category: tags
|
||||
Contributor: authorData {
|
||||
id
|
||||
image {
|
||||
childImageSharp {
|
||||
gatsbyImageData(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
name
|
||||
}
|
||||
featuredImage {
|
||||
childImageSharp {
|
||||
gatsbyImageData(placeholder: NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
377
src/pages/docs/index.tsx
Normal file
377
src/pages/docs/index.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import React from 'react'
|
||||
import { StaticImage } from 'gatsby-plugin-image'
|
||||
import docs from 'sidebars/docs.json'
|
||||
|
||||
import Layout from 'components/Layout'
|
||||
import { SEO } from 'components/seo'
|
||||
import Link from 'components/Link'
|
||||
import PostLayout from 'components/PostLayout'
|
||||
import { LinkGrid } from 'components/Docs/LinkGrid'
|
||||
import { graphql, PageProps } from 'gatsby'
|
||||
|
||||
const quickLinks = [
|
||||
{
|
||||
icon: 'Analytics',
|
||||
name: 'Product analytics',
|
||||
to: '/docs/product-analytics',
|
||||
description: 'Better understand your users and build better products',
|
||||
},
|
||||
{
|
||||
icon: 'SessionRecording',
|
||||
name: 'Session recording',
|
||||
to: '/docs/session-recording',
|
||||
description: 'Play back sessions to diagnose UI issues and get inspired',
|
||||
},
|
||||
{
|
||||
icon: 'FeatureFlags',
|
||||
name: 'Feature flags',
|
||||
to: '/docs/feature-flags',
|
||||
description: 'Toggle features to test the impact before rolling out',
|
||||
},
|
||||
{
|
||||
icon: 'AbTesting',
|
||||
name: 'A/B testing',
|
||||
to: '/docs/experiments',
|
||||
description: 'A/B test UI changes and new features',
|
||||
},
|
||||
{
|
||||
icon: 'DataManagement',
|
||||
name: 'Data',
|
||||
to: '/docs/data',
|
||||
description: 'Get a complete picture of all your data',
|
||||
},
|
||||
{
|
||||
icon: 'AppLibrary',
|
||||
name: 'Apps',
|
||||
to: '/docs/apps',
|
||||
description: 'Extend PostHog by adding your own functionality',
|
||||
},
|
||||
]
|
||||
|
||||
const otherLinks = [
|
||||
{
|
||||
name: 'Integrate PostHog',
|
||||
links: [
|
||||
{ name: 'Send events', to: '/docs/getting-started/send-events' },
|
||||
{ name: 'Historical events', to: '/docs/integrate/ingest-historic-data' },
|
||||
{ name: 'Identifying users', to: '/docs/integrate/identifying-users' },
|
||||
{ name: 'Libraries', to: '/docs/integrate/libraries' },
|
||||
{ name: 'Proxying events', to: '/docs/integrate/proxy' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Self-host',
|
||||
links: [
|
||||
{ name: 'Deployment', to: '/docs/self-host' },
|
||||
{ name: 'Runbook', to: '/docs/runbook' },
|
||||
{ name: 'Environment variables', to: '/docs/self-host/configure/environment-variables' },
|
||||
{ name: 'Monitoring', to: '/docs/self-host/configure/monitoring-with-grafana' },
|
||||
{ name: 'Upgrading', to: '/docs/runbook/upgrading-posthog' },
|
||||
{ name: 'Troubleshooting', to: '/docs/self-host/deploy/troubleshooting' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Apps',
|
||||
links: [
|
||||
{ name: 'Explore app library', to: '/apps' },
|
||||
{ name: 'Use cases', to: '/docs/apps' },
|
||||
{ name: 'Building an app', to: '/docs/apps/build' },
|
||||
{ name: 'Developer reference', to: '/docs/apps/build/reference' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Data management',
|
||||
links: [
|
||||
{ name: 'Organizations & projects', to: '/manual/organizations-and-projects' },
|
||||
{ name: 'UTM parameters', to: '/manual/utm-segmentation' },
|
||||
{ name: 'Notifications & alerts', to: '/manual/notifications-and-alerts' },
|
||||
{ name: 'Events', to: '/manual/events' },
|
||||
{ name: 'Annotations', to: '/manual/annotations' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Developers',
|
||||
links: [
|
||||
{ name: 'REST API', to: '/docs/api' },
|
||||
{ name: 'Developing locally', to: '/handbook/engineering/developing-locally' },
|
||||
{ name: 'Contributing', to: '/docs/contribute' },
|
||||
{ name: 'How PostHog works', to: '/docs/how-posthog-works' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Privacy & compliance',
|
||||
links: [
|
||||
{ name: 'GDPR', to: '/docs/privacy/gdpr-compliance' },
|
||||
{ name: 'HIPAA', to: '/docs/privacy/hipaa-compliance' },
|
||||
{ name: 'CCPA', to: '/docs/privacy/ccpa-compliance' },
|
||||
{ name: 'Data deletion', to: '/docs/privacy/data-deletion' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
type ImportantLinkProps = {
|
||||
to: string
|
||||
icon?: string
|
||||
title: string
|
||||
badge?: 'new' | 'beta' | undefined
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const ImportantLink: React.FC<ImportantLinkProps> = ({ to, icon, title, badge, children }) => {
|
||||
const badgeClass = badge === 'new' ? 'success' : badge === 'beta' ? 'warning' : null
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="text-almost-black hover:text-almost-black dark:text-white dark:hover:text-white font-semibold p-2 hover:bg-gray-accent/40 active:hover:bg-gray-accent/60 dark:hover:bg-gray-accent/10 dark:active:bg-gray-accent/5 rounded flex items-center space-x-2 text-[14px]"
|
||||
to={to}
|
||||
>
|
||||
{icon ? <img src={icon} className="w-5 h-5" /> : children || null}
|
||||
<span>{title}</span>
|
||||
{badge && <span className={`lemon-tag ${badgeClass}`}>{badge}</span>}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
type DocsData = {
|
||||
gettingStarted: {
|
||||
nodes: {
|
||||
fields: {
|
||||
slug: string
|
||||
}
|
||||
frontmatter: {
|
||||
title: string
|
||||
icon?: {
|
||||
publicURL: string
|
||||
}
|
||||
}
|
||||
}[]
|
||||
}
|
||||
sdks: {
|
||||
nodes: {
|
||||
fields: {
|
||||
slug: string
|
||||
}
|
||||
frontmatter: {
|
||||
title: string
|
||||
icon?: {
|
||||
publicURL: string
|
||||
}
|
||||
}
|
||||
}[]
|
||||
}
|
||||
apps: {
|
||||
nodes: {
|
||||
fields: {
|
||||
slug: string
|
||||
}
|
||||
frontmatter: {
|
||||
title: string
|
||||
thumbnail?: {
|
||||
publicURL: string
|
||||
}
|
||||
}
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export const DocsIndex = ({ data }: PageProps<DocsData>) => {
|
||||
const { gettingStarted, sdks, apps } = data
|
||||
|
||||
const gettingStartedLinks = React.useMemo(() => {
|
||||
const gettingStartedSection = docs.find((section) => section.name === 'Getting started')?.children || []
|
||||
return gettingStarted.nodes.sort(
|
||||
(a, b) =>
|
||||
gettingStartedSection?.findIndex((link) => link.url === a.fields.slug) -
|
||||
gettingStartedSection?.findIndex((link) => link.url === b.fields.slug)
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Documentation - PostHog" />
|
||||
|
||||
<PostLayout article={false} title={'Docs'} menu={docs} hideSidebar hideSurvey>
|
||||
<div className="space-y-16 lg:space-y-20 lg:mt-0 mt-8 mb-8">
|
||||
<section className="border-b border-dashed border-gray-accent-light dark:border-gray-accent-dark pb-8">
|
||||
<div className="w-full z-20">
|
||||
<StaticImage
|
||||
objectFit="contain"
|
||||
src="../../../contents/images/search-hog-3.png"
|
||||
alt="This hog has an answer"
|
||||
width={400}
|
||||
placeholder="blurred"
|
||||
className="w-full sm:w-[400px] sm:float-right sm:ml-8 sm:-mt-8 mb-8"
|
||||
/>
|
||||
<h1 className="font-bold text-3xl md:text-4xl lg:text-5xl mb-2 whitespace-nowrap">
|
||||
Documentation
|
||||
</h1>
|
||||
<h5 className="text-lg font-semibold text-primary/60 dark:text-primary-dark/75 leading-tighttext-lg text-gray pb-8">
|
||||
New to PostHog? Visit the{' '}
|
||||
<Link to="/docs/getting-started/start-here" className="font-semibold">
|
||||
getting started guide
|
||||
</Link>
|
||||
.
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<LinkGrid links={quickLinks} />
|
||||
</section>
|
||||
|
||||
<section className="space-y-8">
|
||||
<div className="text-center">
|
||||
<h2 className="font-bold mb-1 inline-block w-full">Quick links</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-x-4 rounded xl:rounded-none overflow-hidden">
|
||||
<div className="bg-gray-accent-light dark:bg-gray-accent-dark lg:rounded px-6 py-4">
|
||||
<div className="mb-3">
|
||||
<h4 className="font-bold mb-0">Getting started</h4>
|
||||
<p className="text-sm text-gray">Complete guide to getting set-up</p>
|
||||
</div>
|
||||
|
||||
<ul className="grid sm:grid-cols-2 xl:grid-cols-1 w-full list-none m-0 p-0 space-y-1">
|
||||
{gettingStartedLinks.map((step, index) => {
|
||||
return (
|
||||
<li className="flex-grow" key={step.fields.slug}>
|
||||
<ImportantLink to={step.fields.slug} title={step.frontmatter.title}>
|
||||
<div className="bg-red text-white w-5 h-5 flex items-center justify-center rounded opacity-60 text-xs">
|
||||
<span>{index + 1}</span>
|
||||
</div>
|
||||
</ImportantLink>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-accent-light dark:bg-gray-accent-dark xl:rounded px-6 py-4">
|
||||
<div className="mb-3">
|
||||
<h4 className="font-bold mb-0">Popular SDKs</h4>
|
||||
<p className="text-sm text-gray">Integrate with your favoriate language</p>
|
||||
</div>
|
||||
<ul className="grid grid-cols-2 xl:grid-cols-1 w-full list-none m-0 p-0 space-y-1">
|
||||
{sdks.nodes.map((sdk) => {
|
||||
return (
|
||||
<li className="flex-grow" key={sdk.fields.slug}>
|
||||
<ImportantLink
|
||||
to={sdk.fields.slug}
|
||||
title={sdk.frontmatter.title}
|
||||
icon={sdk.frontmatter.icon?.publicURL}
|
||||
badge={undefined}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-accent-light dark:bg-gray-accent-dark xl:rounded px-6 py-4">
|
||||
<div className="mb-3">
|
||||
<h4 className="font-bold mb-0">Popular apps</h4>
|
||||
<p className="text-sm text-gray">Import, transform, and export data</p>
|
||||
</div>
|
||||
<ul className="grid sm:grid-cols-2 xl:grid-cols-1 w-full list-none m-0 p-0 space-y-1">
|
||||
{apps.nodes.map((app) => {
|
||||
return (
|
||||
<li className="flex-grow" key={app.fields.slug}>
|
||||
<ImportantLink
|
||||
to={app.fields.slug}
|
||||
title={app.frontmatter.title}
|
||||
icon={app.frontmatter.thumbnail?.publicURL}
|
||||
badge={undefined}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-8">
|
||||
<div className="text-center">
|
||||
<h2 className="font-bold mb-1">More handy links</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 border-t border-l border-dashed border-gray-accent-light dark:border-gray-accent-dark">
|
||||
{otherLinks.map((category) => {
|
||||
return (
|
||||
<div
|
||||
key={category.name}
|
||||
className="space-y-2 py-4 md:py-6 px-4 md:px-8 border-dashed border-b border-r border-gray-accent-light dark:border-gray-accent-dark"
|
||||
>
|
||||
<h4 className="mb-0">{category.name}</h4>
|
||||
<ul className="p-0 space-y-1">
|
||||
{category.links.map((link) => {
|
||||
return (
|
||||
<li key={link.to} className="list-none">
|
||||
<Link to={link.to}>{link.name}</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PostLayout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export const query = graphql`
|
||||
query PopularLinks {
|
||||
gettingStarted: allMdx(
|
||||
filter: { slug: { regex: "/^docs/getting-started/(?!start-here)[\\w\\-]+$/" } }
|
||||
) {
|
||||
nodes {
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
icon {
|
||||
publicURL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sdks: allMdx(
|
||||
filter: { slug: { regex: "/^docs/sdks/(js|node|python|react|ios|android)/$/" } }
|
||||
sort: { fields: fields___pageViews, order: DESC }
|
||||
) {
|
||||
nodes {
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
icon {
|
||||
publicURL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
apps: allMdx(
|
||||
filter: { slug: { regex: "/^docs/apps/(?!build)\\w+/" } }
|
||||
sort: { fields: fields___pageViews, order: DESC }
|
||||
limit: 6
|
||||
) {
|
||||
nodes {
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
thumbnail {
|
||||
publicURL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default DocsIndex
|
||||
167
src/pages/docs/product-analytics.tsx
Normal file
167
src/pages/docs/product-analytics.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import { StaticImage } from 'gatsby-plugin-image'
|
||||
|
||||
import docs from 'sidebars/docs.json'
|
||||
import Layout from 'components/Layout'
|
||||
import { SEO } from 'components/seo'
|
||||
import PostLayout from 'components/PostLayout'
|
||||
import { Tutorials } from 'components/Docs/Tutorials'
|
||||
import { LinkGrid } from 'components/Docs/LinkGrid'
|
||||
import { GettingStarted } from 'components/Docs/GettingStarted'
|
||||
|
||||
export const quickLinks = [
|
||||
{
|
||||
icon: 'Trends',
|
||||
name: 'Graphs & trends',
|
||||
to: '/manual/trends',
|
||||
},
|
||||
{
|
||||
icon: 'Funnels',
|
||||
name: 'Funnels',
|
||||
to: '/manual/funnels',
|
||||
},
|
||||
{
|
||||
icon: 'PathAnalysis',
|
||||
name: 'User paths',
|
||||
to: '/manual/paths',
|
||||
},
|
||||
{
|
||||
icon: 'Dashboards',
|
||||
name: 'Dashboards',
|
||||
to: '/manual/dashboards',
|
||||
},
|
||||
{
|
||||
icon: 'Retention',
|
||||
name: 'Retention',
|
||||
to: '/manual/retention',
|
||||
},
|
||||
{
|
||||
icon: 'Stickiness',
|
||||
name: 'Stickiness',
|
||||
to: '/manual/stickiness',
|
||||
},
|
||||
{
|
||||
icon: 'Lifecycle',
|
||||
name: 'Lifecycle',
|
||||
to: '/manual/lifecycle',
|
||||
},
|
||||
{
|
||||
icon: 'CorrelationAnalysis',
|
||||
name: 'Correlation analysis',
|
||||
to: '/manual/correlation',
|
||||
},
|
||||
{
|
||||
icon: 'GroupAnalytics',
|
||||
name: 'Groups',
|
||||
to: '/manual/group-analytics',
|
||||
},
|
||||
{
|
||||
icon: 'Toolbar',
|
||||
name: 'Toolbar',
|
||||
to: '/manual/toolbar',
|
||||
},
|
||||
{
|
||||
icon: 'Sampling',
|
||||
name: 'Sampling',
|
||||
to: '/manual/sampling',
|
||||
},
|
||||
]
|
||||
|
||||
type ProductAnalyticsProps = {
|
||||
data: {
|
||||
tutorials: {
|
||||
edges: {
|
||||
node: any
|
||||
}[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ProductAnalytics: React.FC<ProductAnalyticsProps> = ({ data }) => {
|
||||
const { tutorials } = data
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Product analytics - Documentation - PostHog" />
|
||||
|
||||
<PostLayout title={'Product Analytics'} menu={docs} hideSurvey hideSidebar>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
quality={100}
|
||||
className="w-full sm:w-[400px] sm:float-right sm:ml-8 sm:-mt-8 sm:mb-8"
|
||||
src="../../components/Home/Slider/images/product-analytics-hog.png"
|
||||
/>
|
||||
<h1 className="text-4xl mb-2 mt-6">Product analytics</h1>
|
||||
<h3 className="text-lg font-semibold text-primary/60 dark:text-primary-dark/75 leading-tight">
|
||||
Learn how to use product analytics to understand your users.
|
||||
</h3>
|
||||
|
||||
{/* Quick links */}
|
||||
<section className="my-12 clear-both">
|
||||
<h3 className="mb-6 mt-0">Chapters</h3>
|
||||
<LinkGrid links={quickLinks} />
|
||||
</section>
|
||||
|
||||
<Tutorials tutorials={tutorials} />
|
||||
</PostLayout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductAnalytics
|
||||
|
||||
export const query = graphql`
|
||||
query ProductAnalytics {
|
||||
tutorials: allMdx(
|
||||
limit: 6
|
||||
sort: { order: DESC, fields: [frontmatter___date] }
|
||||
filter: {
|
||||
frontmatter: {
|
||||
tags: {
|
||||
in: [
|
||||
"cohorts"
|
||||
"actions"
|
||||
"funnels"
|
||||
"group-analytics"
|
||||
"insights"
|
||||
"retention"
|
||||
"user-paths"
|
||||
"toolbar"
|
||||
"trends"
|
||||
]
|
||||
}
|
||||
}
|
||||
fields: { slug: { regex: "/^/tutorials/" } }
|
||||
}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
date(formatString: "MMM 'YY")
|
||||
Category: tags
|
||||
Contributor: authorData {
|
||||
id
|
||||
image {
|
||||
childImageSharp {
|
||||
gatsbyImageData(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
name
|
||||
}
|
||||
featuredImage {
|
||||
childImageSharp {
|
||||
gatsbyImageData(placeholder: NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
125
src/pages/docs/session-recording.tsx
Normal file
125
src/pages/docs/session-recording.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import { StaticImage } from 'gatsby-plugin-image'
|
||||
|
||||
import docs from 'sidebars/docs.json'
|
||||
import Layout from 'components/Layout'
|
||||
import { SEO } from 'components/seo'
|
||||
import PostLayout from 'components/PostLayout'
|
||||
import { Tutorials } from 'components/Docs/Tutorials'
|
||||
import { LinkGrid } from 'components/Docs/LinkGrid'
|
||||
import { GettingStarted } from 'components/Docs/GettingStarted'
|
||||
|
||||
export const quickLinks = [
|
||||
{
|
||||
name: 'Configuration',
|
||||
to: '/docs/session-recording/configure',
|
||||
description: 'Settings for customizing session recording capture.',
|
||||
},
|
||||
{
|
||||
name: 'Data retention',
|
||||
to: '/docs/session-recording/data-retention',
|
||||
description: 'Adjust how long session recordings are stored when self-hosting.',
|
||||
},
|
||||
{
|
||||
name: 'Troublehsooting & FAQs',
|
||||
to: '/docs/session-recording/troublehsooting',
|
||||
description: 'Common issues and how to resolve them.',
|
||||
},
|
||||
]
|
||||
|
||||
type SessionRecordingProps = {
|
||||
data: {
|
||||
tutorials: {
|
||||
edges: {
|
||||
node: any
|
||||
}[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SessionRecording: React.FC<SessionRecordingProps> = ({ data }) => {
|
||||
const { tutorials } = data
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title="Session recording - Docs - PostHog" />
|
||||
|
||||
<PostLayout title={'Session recording'} menu={docs} hideSurvey hideSidebar>
|
||||
<StaticImage
|
||||
alt=""
|
||||
placeholder="none"
|
||||
quality={100}
|
||||
className="w-full sm:w-[400px] sm:float-right sm:ml-8 sm:-mt-8 sm:mb-8"
|
||||
src="../../components/Home/Slider/images/session-recording-hog.png"
|
||||
/>
|
||||
<h1 className="text-4xl mb-2 mt-6">Session recording</h1>
|
||||
<h3 className="text-lg font-semibold text-primary/60 dark:text-primary-dark/75 leading-tight">
|
||||
Play back sessions to diagnose UI issues, improve support, and get context for nuanced user
|
||||
behavior.
|
||||
</h3>
|
||||
|
||||
{/* Get started section */}
|
||||
<section className="pt-4 pb-0">
|
||||
<GettingStarted
|
||||
articleType="Pinned"
|
||||
title="Record your first session"
|
||||
description="Flip a switch to start capturing session recordings in minutes."
|
||||
link="/docs/session-recording/manual#enabling-session-recording"
|
||||
></GettingStarted>
|
||||
</section>
|
||||
|
||||
{/* Quick links */}
|
||||
<section className="my-12">
|
||||
<h3 className="mb-6 mt-0">Quick links</h3>
|
||||
<LinkGrid links={quickLinks} />
|
||||
</section>
|
||||
|
||||
<Tutorials tutorials={tutorials} />
|
||||
</PostLayout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionRecording
|
||||
|
||||
export const query = graphql`
|
||||
query SessionRecording {
|
||||
tutorials: allMdx(
|
||||
limit: 6
|
||||
sort: { order: DESC, fields: [frontmatter___date] }
|
||||
filter: {
|
||||
frontmatter: { tags: { in: ["session recording"] } }
|
||||
fields: { slug: { regex: "/^/tutorials/" } }
|
||||
}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
date(formatString: "MMM 'YY")
|
||||
Category: tags
|
||||
Contributor: authorData {
|
||||
id
|
||||
image {
|
||||
childImageSharp {
|
||||
gatsbyImageData(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
name
|
||||
}
|
||||
featuredImage {
|
||||
childImageSharp {
|
||||
gatsbyImageData(placeholder: NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
File diff suppressed because it is too large
Load Diff
@@ -133,7 +133,7 @@ export default function Product({ data, location }) {
|
||||
<>
|
||||
<h4 className="m-0 mb-9">{title} documentation</h4>
|
||||
<ul className="m-0 p-0 list-none">
|
||||
{documentation.headings?.map((heading) => {
|
||||
{documentation?.headings?.map((heading) => {
|
||||
const id = slugger.slug(heading.value)
|
||||
return (
|
||||
<li key={id}>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"esModuleInterop": true, // Disables namespace imports (import * as fs from "fs") and enables CJS/AMD/UMD style imports (import fs from "fs")
|
||||
"allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
|
||||
"strict": true, // Enable all strict type checking options
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file.
|
||||
"module": "esnext", // Specify module code generation
|
||||
"moduleResolution": "node", // Resolve modules using Node.js style
|
||||
|
||||
16
vercel.json
16
vercel.json
@@ -687,6 +687,10 @@
|
||||
"source": "/blog/introduction-to-customer-retention",
|
||||
"destination": "/blog/customer-churn-analysis-guide"
|
||||
},
|
||||
{
|
||||
"source": "/manual/recordings",
|
||||
"destination": "/docs/session-recording"
|
||||
},
|
||||
{
|
||||
"source": "/docs/getting-started/cloud",
|
||||
"destination": "/docs/getting-started/start-here"
|
||||
@@ -702,6 +706,18 @@
|
||||
{
|
||||
"source": "/docs/self-host/open-source/deployment",
|
||||
"destination": "/docs/self-host"
|
||||
},
|
||||
{
|
||||
"source": "/docs/integrate/identifying-users",
|
||||
"destination": "/docs/getting-started/identify-users"
|
||||
},
|
||||
{
|
||||
"source": "/docs/integrate/user-properties",
|
||||
"destination": "/docs/getting-started/user-properties"
|
||||
},
|
||||
{
|
||||
"source": "/docs/integrate",
|
||||
"destination": "/docs/getting-started/install"
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
|
||||
Reference in New Issue
Block a user