Keeping product comparison charts up-to-date across a large website with multiple products is tricky, so we've built a way to source data from a single place. That way, if a competitor adds a new feature (or updates an existing one), we can update the data in one place and have it automatically reflected across the entire website in existing product comparison tables, blog posts, and other documentation.
To do this, we need a source of record for:
feature definitions (each PostHog product and its feature set)
competitor data (each competitor and their product and feature offerings)
By standardizing all features across all products and competitors, we can generate a comparison table without any hard-coded data.
Example
This is not an ordinary Markdown table. (In fact, it's not Markdown at all!)
Features can live in the features node, or nested inside in a logical grouping. (This is a truncated example.)
typescript
exportconst sessionReplayFeatures ={
summary:{
name:'Session Replay',
description:'Watch real user sessions to understand behavior and fix issues',
url:'/session-replay',
docsUrl:'/docs/session-replay',
},
features:{
canvas_recording:{
name:'Canvas recording',
description:'Capture canvas elements in your app',
},
chat_with_recordings:{
name:'Chat with your recordings',
description:'Discover useful recordings using AI-powered chat',
},
},
platform_support:{
description:'Record on web and mobile across major frameworks',
features:{
web_app_recordings:{
name:'Web app recordings',
description:'Capture recordings from single-page apps and websites',
},
mobile_app_recordings:{
name:'Mobile app recordings',
description:'Capture recordings in iOS and Android apps',
},
ios_recordings:{
name:'iOS recordings',
description:'Record sessions from iOS mobile apps',
},
},
},
}
Competitor (& PostHog) data
Competitor data is stored in:
/src/hooks/competitorData/{competitorName}.tsx
/src/hooks/competitorData/posthog.tsx
Amplitude example:
/src/hooks/competitorData/amplitude.tsx
Feature-level data for competitors is stored in the same format, with the exception being that products are namespaced under the products node in a single file instead of being spread across multiple files for each product.
There's also a platform node below the product array.
typescript
exportconst amplitude ={
name:'Amplitude',
key:'amplitude',
assets:{
icon:'/images/competitors/amplitude.svg',
comparisonArticle:'/blog/posthog-vs-amplitude',
},
products:{
session_replay:{
available:true,
pricing:{
free_tier:'1,000 recordings',
},
features:{
canvas_recording:false,
chat_with_recordings:false,
clickmaps:false,
conditional_recording:false,
},
},
},
platform:{
deployment:{
eu_hosting:true,
open_source:false,
self_host:false,
},
pricing:{
free_tier:true,
transparent_pricing:false,
usage_based_pricing:true,
},
},
}
Referencing data
There are several ways to assemble competitor tables. It uses the <ProductComparisonTable /> component which uses <OSTable /> internally.
Compare products between competitors
This will list out the top-level product names and descriptions.
If you want to cherry-pick specific features, just reference the key directly. (This is useful for blog posts that compare specific features between competitors in a manually set order.)
The values array should have the same length as the competitors array, with each value corresponding to a competitor in order.
Section headers
Add section headers to organize comparison tables into logical groups. Headers only require a label property:
TSX
<ProductComparisonTable
competitors={['posthog','amplitude']}
rows={[
{label:'Core Features'},// Section header - no description needed
'product_analytics.features.autocapture',
'product_analytics.features.cohorts',
{label:'Advanced Features'},// Another section header
'product_analytics.insights.sql_editor',
'product_analytics.group_analytics',
]}
/>
Headers automatically span across all columns and are styled with a border to visually separate sections.
Product page overrides
Excluding sections
Product pages list out all sections within a product's feature set by default, but in some cases it doesn't make sense to do so.
For example, showing the platform.integrations section might make sense for the Product Analytics comparison, but not for LLM Analytics comparison where that product doesn't really integrate with the tools that are otherwise integrated across the PostHog platform.
If you want to exclude a section from rendering, you can use the excludedSections property.
TSX
<ProductComparisonTable
competitors={['posthog','amplitude']}
rows={['product_analytics.features']}
excludedSections={['platform']}
/>
For product pages, this is handled by the excluded_sections property in the product's feature definition file.
/src/hooks/productData/llm_analytics.tsx:
typescript
exportconst llmAnalytics ={
...
comparison:{
companies:[
{
name:'Langfuse',
key:'langfuse',
},
{
name:'Langsmith',
key:'langsmith',
},
{
name:'Helicone',
key:'helicone',
},
{
name:'PostHog',
key:'posthog',
},
],
rows:['llm_analytics'],
excluded_sections:['platform'],// or an individual node like 'platform.integrations'
},
}
Excluding rows with missing data
By default, the component will show rows where a competitor's cell doesn't have a value. This can be overridden on a per-product basis by setting require_complete_data: true in the product's feature definition file.