merged master

This commit is contained in:
Cory Watilo
2025-11-10 10:07:30 -05:00
85 changed files with 1801 additions and 629 deletions

View File

@@ -1,6 +1,6 @@
---
title: 6 best error tracking tools for developers
date: 2025-11-27
title: The best error tracking tools for developers, compared
date: 2025-11-07
rootPage: /blog
sidebar: Blog
showTitle: true
@@ -12,31 +12,41 @@ featuredImage: >-
featuredImageType: full
category: General
tags:
- Alternatives
- Comparisons
seo:
{
metaTitle: '6 best error tracking tools for developers',
metaTitle: 'The best error tracking tools for developers, compared',
metaDescription: 'Compare the top error tracking tools for developers, including PostHog, Sentry, and SigNoz. See which platforms offer the best context, speed, and control.',
}
---
Every developer has shipped a bug that slipped through tests. What matters isn't perfection, it's how quickly you find out, understand the cause, and ship a fix. Great error tracking tools give you context, not just stack traces: they show what the user did right before the exception, which release it came from, and what else broke as a result.
import { CalloutBox } from 'components/Docs/CalloutBox'
The top error monitoring tools give you this context, and in this guide, we'll compare the best options for developers across open-source options, enterprise suites, and all-in-one platforms.
Every developer has shipped a bug that slipped through tests: no one is perfect. What matters is how quickly you find out, understand the cause, and ship a fix. Enter, error tracking tools.
## What are the features of a great error tracking tool
A good error tracking tool will give you context, not just stack traces: it'll show you what the user did right before the exception, which release it came from, and what else broke as a result.
Before jumping into the list, here are the features we are looking for in each tool:
This guide compares the most popular error tracking tools for developers right now, what features they do and don't offer, and who they're built for, so you can decide which one is right for your needs.
## What features do you need in your error tracking tool?
At a minimum, most good error tracking tools will offer things like:
- Real-time error capture and grouping
- Full stack traces, source maps, and contextual metadata
- Alerts and assignment workflows for triage
- SDKs for major languages and frameworks
- Integration with CI/CD, logs, and performance data
- Clear pricing and scalability
- Behavioral data context (session replays, user data, or analytics)
## Quick comparison
The absolute best error tools tend to go a bit further by including things like:
- **Integration with session replay:** Useful for linking session recordings to real user sessions, so you can better understand the context of when an error occurs and debug the steps.
- **Integration with product analytics:** Useful so you can correlate the impact of errors on product usage, conversion, and revenue.
- **Very broad SDK and framework support:** Support for the likes of Next.js, Python, and React are typical among all the tools in this list, but some tools go deeper by offering SDKs for less popular frameworks, and even video game engines like Unreal Engine and Unity.
Here's how some of the most popular error tracking tools compare at a glance:
<ProductComparisonTable
competitors={['posthog', 'sentry', 'rollbar', 'bugsnag', 'glitchtip', 'signoz']}
@@ -49,50 +59,48 @@ Before jumping into the list, here are the features we are looking for in each t
'error_tracking.monitoring.performance_monitoring',
'error_tracking.features.console_log_capture',
'error_tracking.features.mobile_sdk_coverage',
{ label: 'Platform benefits' },
'platform.deployment.self_host',
'platform.pricing.usage_based_pricing',
'platform.integrations.ci_cd_integrations',
'error_tracking.integrations.product_analytics',
'platform.deployment.open_source',
]}
/>
### 1. PostHog: Best for contextual debugging and developer control
## What's the best error tracking tool for developers?
**TL;DR:** Most error trackers stop at stack traces, but PostHog goes further. It combines [error tracking](/error-tracking), [session replay](/session-replay), [analytics](/product-analytics), and [feature flags](/feature-flags) in one platform, giving developers full context on every issue.
### 1. PostHog
Error tracking is only one part of building great products, and that's the real advantage of PostHog. It's not a tool you bolt on after the fact, it's part of the feedback loop between shipping, learning, and improving. Each exception can be linked to the related session replay, user, and feature flag version. You can see the user's actions, console output, and API calls leading up to the issue, then ship a fix behind a feature flag, test it, and measure the impact in analytics.
PostHog is an all-in-one developer platform that goes beyond error tracking and stack traces by combining [error tracking](/error-tracking), [session replay](/session-replay), [analytics](/product-analytics), and [feature flags](/feature-flags) into one platform.
You're also able to autocapture unhandled exceptions, filter by error type, and set event-based alerts that trigger when specific issues occur.
Each exception can be linked to the related session replay, user, and feature flag version. You can also view the user's actions, console output, and API calls leading up to the issue, then ship a fix behind a feature flag, test it, and measure the impact in analytics without ever having to switch between tools.
**Strengths**
PostHog also [supports autocapture of unhandled exceptions](/docs/error-tracking/capture), [filter by error type](/docs/error-tracking/monitoring), and [set event-based alerts](/docs/error-tracking/alerts) that trigger when specific issues occur, and offers built in [LLM analytics](/llm-analytics) for teams that need to observe and optimize AI products as well.
**Strengths:**
- Replay-linked debugging
- Full user and session context
- Transparent usage-based pricing with configurable caps
- Unified suite: analytics, feature flags, surveys, experiments, and more
- Unified suite: analytics, feature flags, surveys, experiments, LLM analytics and more
**Best for:** Teams that want an integrated view of errors, user behavior, and product analytics in one place.
**Community:**
<BorderWrapper>
<Quote
imageSource="/images/customers/brandon.png"
size="md"
name="Brandon Jakobson"
title="Co-founder & CTO at Zealot"
quote={`“I can look at an error and see everyone who had it, then view their replays in two clicks. That's the part about PostHog that's so cool: you get all these tools for free, and the more you use, the more powerful they become.”`}
/>
</BorderWrapper>
- PostHog is fully open source under the MIT license and actively maintained at https://github.com/PostHog/posthog.
**Community:**
PostHog is fully open source under the MIT license and actively maintained at [github.com/PostHog/posthog](https://github.com/PostHog/posthog). The repository has 29.8k+ stars, 360+ contributors, and sees daily commits from both the core team and community. Most development happens in public, including product discussions and roadmap planning.
- The [repository](https://github.com/PostHog/posthog) has 29.8k+ stars, 360+ contributors, and sees daily commits from both the core team and community.
### 2. Sentry: Best for production-scale error and performance tracking
- [Most development happens in public](/changelog), including product discussions and roadmap planning.
**TL;DR:** Sentry is a mature, battle-tested error and performance monitoring tool used across industries. It's stable, deeply integrated, and built for teams who value visibility over novelty.
<CalloutBox icon="IconStarFilled" title="PostHog is best for" type="fyi">
Sentry has earned its reputation by being the tool developers turn to when uptime and reliability are non-negotiable. It provides rich grouping, detailed stack traces, breadcrumbs, release tracking, and performance monitoring across frontend, backend, and mobile SDKs. It's highly scalable, with strong alerting and triage workflows.
Teams that want an integrated view of errors, user behavior, and product analytics in one place. It's also a great choice for any team that's building AI apps, since you get error tracking and an [LLM observability tool](/blog/best-open-source-llm-observability-tools) inside one platform as well.
</CalloutBox>
### 2. Sentry
Sentry is a mature, battle-tested error and performance monitoring tool used across industries. It's stable, deeply integrated, and built for teams who value visibility over novelty.
[Sentry](/blog/posthog-vs-sentry) has earned its reputation by being the tool developers turn to when uptime and reliability are non-negotiable. It provides rich grouping, detailed stack traces, breadcrumbs, release tracking, and performance monitoring across frontend, backend, and mobile SDKs.
It's highly scalable, with strong alerting and triage workflows.
**Strengths**
@@ -101,18 +109,27 @@ Sentry has earned its reputation by being the tool developers turn to when uptim
- Self-host option available
- Mature ecosystem and integrations
**Best for:** Established teams running large-scale web or mobile applications needing reliability and deep insight.
**Community**
Sentry is one of the most-starred monitoring tools, with ~42k stars and 800+ contributors on [GitHub](https://github.com/getsentry/sentry). It's licensed under the Business Source License (BSL), meaning the core is source-available with usage limits. The team is highly active with weekly releases and a strong community presence.
### 3. Rollbar: Best for CI/CD and release-aware workflows
- Sentry is licensed under the Business Source License (BSL), meaning the core is source-available with usage limits.
**TL;DR:** Rollbar connects errors directly to deploys, releases, and regressions. It's built for teams who deploy constantly and need to know the moment something breaks in production.
- It's one of the [most-starred monitoring tools](https://github.com/getsentry/sentry), with ~42k stars and 800+ contributors on GitHub.
Rollbar's superpower is speed. It specializes in tracking when new releases cause errors, and integrates with GitHub, GitLab, Jira, and Slack, automatically associating new exceptions with recent deployments. This tight loop between code and error makes it a favorite for fast-moving CI/CD teams.
- The team is highly active with weekly releases and there's a large selection of community supported SDKs as well.
You'd pick Rollbar if you value velocity and automation over depth. It's the tool that helps small teams ship multiple times a day without fear.
<CalloutBox icon="IconStarFilled" title="Sentry is best for" type="fyi">
Established teams running large-scale web or mobile applications needing reliability and deep insight, or game developers working with the Unreal and Unity engines.
</CalloutBox>
### 3. Rollbar
Rollbar connects errors directly to deploys, releases, and regressions. It's built for teams who deploy constantly and need to know the moment something breaks in production.
Their superpower is speed. Rollbar specializes in tracking when new releases cause errors, and integrates with GitHub, GitLab, Jira, and Slack, automatically associating new exceptions with recent deployments.
If you value velocity and automation over depth, this is a tool that can help you ship multiple times a day without fear.
**Strengths**
@@ -120,16 +137,25 @@ You'd pick Rollbar if you value velocity and automation over depth. It's the too
- Strong CI/CD and issue tracker integrations
- Automations for regression alerts and triage
**Best for:** Teams deploying continuously who need visibility into which release introduced an error.
**Community**
Rollbar maintains several open SDK repositories across languages, including [rollbar.js](https://github.com/rollbar/rollbar.js), [rollbar-python](https://github.com/rollbar/pyrollbar), and [rollbar-java](https://github.com/rollbar/rollbar-java). Each has hundreds of stars and regular maintenance, though the core product itself is proprietary.
### 4. Bugsnag: Best for mobile stability and crash analytics
- Rollbar maintains several open SDK repositories across languages, including [rollbar.js](https://github.com/rollbar/rollbar.js), [rollbar-python](https://github.com/rollbar/pyrollbar), and [rollbar-java](https://github.com/rollbar/rollbar-java).
**TL;DR:** A leading tool for mobile and frontend error monitoring focused on app stability metrics.
- Each repo has hundreds of stars and regular maintenance, though the core product itself is proprietary.
Bugsnag was built around the insight that not every error is equal. It measures crash-free sessions, calculates stability scores, and surfaces the most impactful issues first so teams can prioritize issues that have the greatest impact on real-world experience. Its clean dashboards and SDK coverage across iOS, Unity, React Native, Android, and web make it a go-to for app developers.
<CalloutBox icon="IconStarFilled" title="Rollbar is best for" type="fyi">
Fast-moving engineering teams that ship code multiple times a day and need to pinpoint exactly which commit, branch, or release introduced a new exception.
</CalloutBox>
### 4. Bugsnag
Bugsnag is a leading tool for mobile and frontend error monitoring focused on app stability metrics.
It was built around the insight that not every error is made equal; it measures crash-free sessions, calculates stability scores, and surfaces the most impactful issues first so teams can prioritize issues that have the greatest impact on real-world experience.
Their clean dashboards and SDK coverage across iOS, Unity, React Native, Android, and web make it a go-to for app developers.
**Strengths**
@@ -137,16 +163,25 @@ Bugsnag was built around the insight that not every error is equal. It measures
- Stability and health metrics help prioritize fixes
- Integrates with common mobile CI/CD pipelines
**Best for:** Mobile and frontend teams focused on improving user stability and retention.
**Community**
Bugsnag's core platform is closed source, but it maintains open SDKs for most major platforms ([JavaScript](https://github.com/bugsnag/bugsnag-js), [Android](https://github.com/bugsnag/bugsnag-android), [Unity](https://github.com/bugsnag/bugsnag-unity), and others). Each repo has hundreds to a few thousand stars, and updates are frequent.
### 5. GlitchTip: Best lightweight open-source error tracker
- Bugsnag's core platform is closed source, but it maintains open SDKs for most major platforms ([JavaScript](https://github.com/bugsnag/bugsnag-js), [Android](https://github.com/bugsnag/bugsnag-android), [Unity](https://github.com/bugsnag/bugsnag-unity), and others).
**TL;DR:** GlitchTip is an open-source, privacy-friendly alternative to Sentry. It's the “just works” open-source tracker: lightweight, predictable, and yours to run however you want.
- Each repo has hundreds to a few thousand stars, and updates are frequent.
GlitchTip provides basic error tracking and self-hosting flexibility for small teams. Its Sentry API compatibility means you can often switch without changing SDKs. GlitchTip's simplicity is also its strength: no over-engineered dashboards, no surprise upgrades, no opaque billing just a clean UI, grouped errors, and self-hosted reliability. It's a compelling choice for smaller engineering teams or privacy-sensitive organizations.
<CalloutBox icon="IconStarFilled" title="Bugsnag is best for" type="fyi">
Mobile and frontend teams focused on improving user stability and retention by combining error data with usage metrics.
</CalloutBox>
### 5. GlitchTip
GlitchTip is an open-source, privacy-friendly alternative to Sentry. It's a lightweight, predictable error tracker that's yours to run however you want.
Its Sentry API compatibility means you can often switch without changing SDKs. GlitchTip's simplicity is also its strength: no over-engineered dashboards, no surprise upgrades, no opaque billing just a clean UI, grouped errors, and self-hosted reliability for small teams.
It's a compelling choice for smaller engineering teams or privacy-sensitive organizations.
**Strengths**
@@ -154,16 +189,23 @@ GlitchTip provides basic error tracking and self-hosting flexibility for small t
- Free and privacy-friendly
- Maintains Sentry protocol compatibility
**Best for:** Small teams or organizations that value control and simplicity over feature depth.
**Community**
[GlitchTip](https://mastodon.online/@glitchtip) is fully open source under the MIT license, with active contributions from the developer community. It's a lightweight alternative to Sentry with regular maintenance and transparent development.
### 6. SigNoz: Best open-source APM and error tracking combination
- [GlitchTip](https://mastodon.online/@glitchtip) is fully open source under the MIT license, with active contributions from the developer community.
**TL;DR:** SigNoz is an open-source observability platform built on OpenTelemetry.
- It's a lightweight alternative to Sentry with regular maintenance and transparent development.
SigNoz collects metrics, traces, and errors into one open-source platform designed to replace proprietary APM tools like Datadog or New Relic. It's self-hostable, cost-effective, and ideal for teams standardizing on open telemetry stacks.
<CalloutBox icon="IconStarFilled" title="GlitchTip is best for" type="fyi">
Small teams or organizations that value control and simplicity over feature depth.
</CalloutBox>
### 6. SigNoz
SigNoz is an open-source observability platform built on OpenTelemetry.
It collects metrics, traces, and errors into one open-source platform designed to replace proprietary APM tools like Datadog or New Relic. It's self-hostable, cost-effective, and ideal for teams standardizing on open telemetry stacks.
**Strengths**
@@ -171,30 +213,26 @@ SigNoz collects metrics, traces, and errors into one open-source platform design
- OpenTelemetry-native and vendor-neutral
- No licensing lock-in
**Best for:** Backend or infrastructure teams who prefer open frameworks and self-hosted observability.
**Community**
[SigNoz](https://github.com/SigNoz/signoz) is open source under the Apache 2.0 license, with 24k+ stars and a rapidly growing contributor base. It's one of the most active OpenTelemetry-native observability projects and ships frequent updates.
## How PostHog is different
- [SigNoz](https://github.com/SigNoz/signoz) is open source under the Apache 2.0 license, with 24k+ stars and a rapidly growing contributor base.
Two words: context and consolidation. Where other tools isolate data, PostHog combines everything developers need:
- It's one of the most active OpenTelemetry-native observability projects and ships frequent updates.
- **All-in-one toolkit:** [Product analytics](https://posthog.com/product-analytics), [web analytics](https://posthog.com/web-analytics), [session replay](https://posthog.com/session-replay), [feature flags](https://posthog.com/feature-flags), [experiments](https://posthog.com/experiments), [surveys](https://posthog.com/surveys), [LLM analytics](https://posthog.com/llm-analytics), and [error tracking](https://posthog.com/error-tracking).
- **Developer-first:** Transparent APIs, SQL query builder, open source, and public roadmap.
- **Transparent pricing:** Generous free tiers and [usage-based billing.](https://posthog.com/pricing)
- **Trusted by engineering teams:** Used by [Supabase, Lovable, ElevenLabs, ResearchGate, and more.](https://posthog.com/customers)
<CalloutBox icon="IconStarFilled" title="SigNoz is best for" type="fyi">
## Takeaways
Backend or infrastructure teams who prefer open frameworks and self-hosted observability.
| Use case | Best tool |
| ----------------------------------- | --------- |
| All-in-one product + error tracking | PostHog |
| Deep performance and error triage | Sentry |
| Release and CI/CD visibility | Rollbar |
| Mobile crash analytics | Bugsnag |
| Simple, self-hosted tracking | GlitchTip |
| Open-source APM | SigNoz |
</CalloutBox>
## Which error tracking tool should you choose?
* Want an all-in-one platform that connects errors to user sessions, analytics, feature flags, experiments, and more? Go with **[PostHog](/error-tracking)**.
* Need deep stack traces, transaction tracing, and mature triage workflows? **Sentry** is the proven choice.
* Shipping constantly and need instant visibility into which release broke what? Choose **Rollbar**.
* Focused on mobile or frontend stability metrics like crash-free sessions? **Bugsnag** is a good fit.
* Want a simple, self-hosted option compatible with Sentry clients? Try **GlitchTip**.
* Prefer an open-source observability stack built on OpenTelemetry? **SigNoz** is probably the answer.
## FAQ
@@ -247,6 +285,17 @@ If your focus is on application-level errors and user experience rather than inf
</details>
{' '}
<details>
<summary>How is PostHog different from other error tracking tools?</summary>
<ArrayCTA />
PostHog is more than an error tracker it gives developers full context by combining [all the tools needed to build a successful product](/products) in one platform.
- **All-in-one toolkit:** [Product analytics](/product-analytics), [web analytics](/web-analytics), [session replay](/session-replay), [feature flags](/feature-flags), [experiments](/experiments), [surveys](/surveys), [LLM analytics](/llm-analytics), and [error tracking](/error-tracking)
- **Developer-first:** Transparent APIs, SQL query builder, open source, and a public roadmap
- **Transparent pricing:** Generous free tiers and [usage-based billing](/pricing)
- **Trusted by teams:** Used by [Supabase, Lovable, ElevenLabs, ResearchGate](/customers), and more
</details>
<br />
<ArrayCTA />

View File

@@ -159,4 +159,8 @@ Its hosted version has a free tier, which offers 10k events per month with 30 da
#### What makes Lunary special?
Lunary is purpose built for LLM chatbots like knowledge bases and support tools. This shows in their focus on features like PII masking, access management, human reviewing, and multi-modal support.
Lunary is purpose built for LLM chatbots like knowledge bases and support tools. This shows in their focus on features like PII masking, access management, human reviewing, and multi-modal support.
## Related reading
If youre also instrumenting the rest of your app (not just the LLM parts), we have a separate roundup of the [best error tracking tools](/blog/best-error-tracking-tools). It compares the more traditional app monitoring tools — useful if you want LLM traces and regular exception monitoring to live side by side.

View File

@@ -486,3 +486,8 @@ We're biased, obviously, but we think PostHog is the perfect Sentry replacement
- You want to try before you buy. We're self-serve with a [generous free tier](/pricing).
Check out [error tracking install guide](/docs/error-tracking/installation) and [read our docs](/docs) to learn more.
## Related reading
If you're still mapping out your monitoring stack, we also have a broader roundup of the [best error tracking tools](/blog/best-error-tracking-tools) available today. That one looks beyond Sentry to compare tools with capabilities such as session replay, product analytics, and full observability.

View File

@@ -233,7 +233,7 @@ Every PostHog user gets a generous amount of free usage each month:
| **Feature flags and A/B testing** | 1 million API requests |
| **Data warehouse** | 1 million synced rows |
Beyond this, we have usage-based pricing with volume discounts. Starts can also qualify for free credits.
Beyond this, we have usage-based pricing with volume discounts. Startups can also qualify for free credits.
For all the details about how much PostHog might cost, see [our pricing page](/pricing).
@@ -260,3 +260,7 @@ You can get started with PostHog in less than 90 seconds using our [AI install w
### Can I use PostHog with a CDP?
Yes. You can use PostHog with Segment, Rudderstack, and more. See our docs on using [PostHog with a CDP](/docs/advanced/cdp) for more.
### What are other error monitoring tool alternatives?
If you want a broader look at what's out there, take a look at our full roundup of the [best error tracking tools](/blog/best-error-tracking-tools) available today. It compares different developer platforms side by side — from simple alerting tools to full observability suites — to help you find the right fit for your stack.

View File

@@ -3,6 +3,8 @@ date: 2025-11-03
title: Workflows are now in Alpha and I already broke mine
author:
- sara-miteva
featuredImage: >-
https://res.cloudinary.com/dmukukwp6/image/upload/this_is_fine_6336efb0ae.jpg
rootPage: /blog
sidebar: Blog
showTitle: true

View File

@@ -105,6 +105,10 @@ The schema of the model as created in BigQuery is:
The BigQuery table will contain one row per `(team_id, distinct_id)` pair, and each pair is mapped to their corresponding `person_id` and latest `properties`. The `properties` field can be either `STRING` or `JSON`, depending on whether the corresponding checkbox is marked or not when creating the batch export.
### Sessions model
You can view the schema for the sessions model in the configuration form when creating a batch export (there are a few too many fields to display here!).
## Creating the batch export
1. Click [Data pipelines](https://app.posthog.com/pipeline) in the navigation and go to the **Destinations** tab.

View File

@@ -183,6 +183,9 @@ The schema of the model as created in Databricks is:
The Databricks table will contain one row per `(team_id, distinct_id)` pair, and each pair is mapped to their corresponding `person_id` and latest `properties`.
### Sessions model
You can view the schema for the sessions model in the configuration form when creating a batch export (there are a few too many fields to display here!).
## FAQ

View File

@@ -89,10 +89,6 @@ More information, including any additional necessary permissions, schema informa
### Sessions model
> **Note:** The sessions model is currently in `beta`. This means the schema of this model is subject to change.
>
> You can request access via the [in-app support form](https://us.posthog.com/#panel=support%3Asupport%3Abatch_exports%3Alow%3Atrue).
A [session](/docs/data/sessions) in PostHog represents a series of events that make up a single use of your product or visit to your website. Each session is uniquely identified by its `session_id`.
If you want to link events with sessions, you'll need to ensure you're capturing the `$session_id` in your events. This is done automatically by our JavaScript Web library and mobile SDKs. For server-side SDKs, refer to our [docs](/docs/data/sessions#server-sdks-and-sessions).

View File

@@ -66,6 +66,10 @@ The schema of the model as created in Postgres is:
The Postgres table will contain one row per `(team_id, distinct_id)` pair, and each pair is mapped to their corresponding `person_id` and latest `properties`.
### Sessions model
You can view the schema for the sessions model in the configuration form when creating a batch export (there are a few too many fields to display here!).
## Creating the batch export
1. Click [Data pipelines](https://app.posthog.com/pipeline) in the navigation and go to the **Destinations** tab.

View File

@@ -81,3 +81,7 @@ The schema of the model as created in Redshift is:
| created_at | `TIMESTAMP WITH TIME ZONE` | The timestamp when the person was created |
The Redshift table will contain one row per `(team_id, distinct_id)` pair, and each pair is mapped to their corresponding `person_id` and latest `properties`.
### Sessions model
You can view the schema for the sessions model in the configuration form when creating a batch export (there are a few too many fields to display here!).

View File

@@ -124,6 +124,10 @@ CREATE TABLE IF NOT EXISTS "{database}"."{schema}"."{table_name}" (
COMMENT = 'PostHog persons table'
```
### Sessions model
You can view the schema for the sessions model in the configuration form when creating a batch export (there are a few too many fields to display here!).
#### How is the persons model kept up to date?
Exporting mutable data (like the persons model) requires executing a merge operation to apply new updates to existing rows. Executing this merge operation in Snowflake involves the following steps:

View File

@@ -15,10 +15,12 @@ To link Postgres:
The data warehouse then starts syncing your Postgres data. You can see details and progress in the [sources tab](https://us.posthog.com/pipeline/sources).
> **Permissions** The Postgres source only requires read permissions on the schemas and tables you intend to sync.
> **Looking for an example of the Postgres source?** Check out our tutorial where we [connect and query Supabase data](/tutorials/supabase-query).
import InboundIpAddresses from '../../_snippets/inbound-ip-addresses.mdx'
<InboundIpAddresses />
> **Note:** We currently don't support connections using IPv6, therefore, you may need to enable IPv4 connections to your database. This is required for Supabase, for example.
> **Note:** We currently don't support connections using IPv6, therefore, you may need to enable IPv4 connections to your database. This is required for Supabase, for example.

View File

@@ -1,21 +1,22 @@
---
title: Track external issues in GitHub and Linear
title: Track external issues in GitHub, Linear, and GitLab
---
import Tab from "components/Tab"
Other than assigning issues to team members in PostHog, you can also create issues in other tracking systems you use like GitHub Issues or Linear.
Other than assigning issues to team members in PostHog, you can also create issues in other tracking systems you use like GitHub Issues, Linear, or GitLab.
> If you use another issue tracking system and would like to request it, [let us know in-app](https://app.posthog.com#panel=support%3Afeedback%3Aerror_tracking%3Alow%3Atrue).
## Setting up external tracking
You can set up external tracking in the [error tracking settings](https://app.posthog.com/project/configuration?tab=error-tracking-integrations). Click **Connect workspace** and follow the prompts to connect your issue tracking system.
You can set up external tracking in the [error tracking settings](https://app.posthog.com/error_tracking/configuration?tab=error-tracking-integrations#selectedSetting=error-tracking-integrations). Select your issue tracking system and follow the instructions.
<Tab.Group tabs={['GitHub', 'Linear']}>
<Tab.Group tabs={['GitHub', 'Linear', 'GitLab']}>
<Tab.List>
<Tab>GitHub</Tab>
<Tab>Linear</Tab>
<Tab>GitLab</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
@@ -33,6 +34,15 @@ PostHog connects to Linear using a [third-party app](https://linear.app/docs/thi
- Create issues for your workspace
- Read access to your workspace
</Tab.Panel>
<Tab.Panel>
PostHog connects to GitLab using a [project access token](https://docs.gitlab.com/user/project/settings/project_access_tokens). PostHog needs the following from GitLab:
- Hostname
- Project ID
- A project access token with the `api` scope
</Tab.Panel>
</Tab.Panels>
</Tab.Group>

View File

@@ -2,6 +2,7 @@
import AgentPrompt from "../../integrate/_snippets/agent-prompt.mdx"
import DetailSetUpReverseProxy from "../../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../../integrate/_snippets/details/posthog-ips.mdx"
Install PostHog in seconds with our wizard by running this command in your project directory with your terminal (it also works for [LLM coding agents](/blog/envoy-wizard-llm-agent) like Cursor and Bolt):
@@ -15,4 +16,6 @@ Check out the wizard's [GitHub repo](https://github.com/PostHog/wizard) for more
<DetailSetUpReverseProxy />
<DetailGroupProductsInOneProject />
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />

View File

@@ -0,0 +1,12 @@
<details>
<summary>Add IPs to Firewall/WAF allowlists (recommended)</summary>
For certain features like [heatmaps](/docs/toolbar/heatmaps), your Web Application Firewall (WAF) may be blocking PostHogs requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site.
**EU**: `3.75.65.221`, `18.197.246.42`, `3.120.223.253`
**US**: `44.205.89.55`, `52.4.194.122`, `44.208.188.173`
These are public, stable IPs used by PostHog services (e.g., Celery tasks for snapshots).
</details>

View File

@@ -1,6 +1,8 @@
import Snippet from "../snippet.mdx"
import DetailGroupProductsInOneProject from "./details/group-products-in-one-project.mdx"
import DetailSetUpReverseProxy from "./details/set-up-reverse-proxy.mdx"
import DetailPostHogIPs from "./details/posthog-ips.mdx"
This is the simplest way to get PostHog up and running. It only takes a few minutes.
@@ -18,6 +20,8 @@ Once the snippet is added, PostHog automatically captures `$pageview` and [other
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
<details>
<summary>Include ES5 support (optional)</summary>

View File

@@ -7,6 +7,7 @@ icon: >-
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import InstallNodePackageManagers from "../integrate/_snippets/install-node-package-managers.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about traffic and usage of your [Angular](https://angular.dev/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
@@ -28,6 +29,8 @@ import AngularInstall from "../integrate/_snippets/install-angular.mdx"
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Tracking pageviews
PostHog automatically tracks your pageviews by hooking up to the browser's `navigator` API as long as you initialize PostHog with the `defaults` config option set after `2025-05-24`.

View File

@@ -6,6 +6,7 @@ icon: >-
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about traffic and usage of your [Astro](https://astro.build/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
@@ -25,6 +26,8 @@ import AstroInstall from "../integrate/_snippets/install-astro.mdx"
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Next steps
For any technical questions for how to integrate specific PostHog features into Astro (such as analytics, feature flags, A/B testing, surveys, etc.), have a look at our [JavaScript Web SDK docs](/docs/libraries/js/features).

View File

@@ -6,6 +6,7 @@ icon: >-
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about traffic and usage of your [Bubble](https://bubble.io/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
@@ -32,6 +33,8 @@ Go to the **SEO / metatags** tab in site settings. Paste your PostHog snippet in
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Capture custom events
To capture custom events, you need to install the [Bubble Toolbox plugin](https://bubble.io/plugin/toolbox-1488796042609x768734193128308700). This enables you to run custom JavaScript code.

View File

@@ -8,6 +8,7 @@ tags:
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
To easily track your Docusaurus site, you can install the [PostHog plugin](https://github.com/PostHog/posthog-docusaurus). This enables you to autocapture pageviews, clicks, session replays, as well as use the other features of PostHog such as [surveys](/docs/surveys).
@@ -49,4 +50,6 @@ Run your site again to see events autocaptured by PostHog.
<DetailSetUpReverseProxy />
<DetailGroupProductsInOneProject />
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />

View File

@@ -6,6 +6,7 @@ icon: >-
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about traffic and usage of your [Framer](https://www.framer.com/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
@@ -21,6 +22,8 @@ import FramerInstall from "../integrate/_snippets/install-framer.mdx"
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Capture custom events
To [capture custom events](/docs/product-analytics/capture-events), you call `posthog.capture()` using [custom code components in Framer](https://www.framer.com/developers/components/introduction).

View File

@@ -6,6 +6,7 @@ icon: >-
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
> This [library](https://github.com/posthog/gatsby-plugin-posthog) was built by the community. Thanks to [Ritesh Kadmawala](https://github.com/kgritesh) for building it.
@@ -55,4 +56,6 @@ For more instructions, see [browser JS library](/docs/integrate/client/js).
<DetailSetUpReverseProxy />
<DetailGroupProductsInOneProject />
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />

View File

@@ -16,6 +16,7 @@ features:
import DetailSetUpReverseProxy from "../../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about traffic and usage of your [Next.js](https://nextjs.org/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
@@ -46,6 +47,8 @@ import NextJSInstall from "../../integrate/_snippets/nextjs/install-nextjs.mdx"
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Accessing PostHog
import Tab from "components/Tab"

View File

@@ -6,6 +6,7 @@ icon: >-
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about usage of your [Nuxt.js](https://nuxt.com/) app. Integrating PostHog into your app enables analytics about user behavior, custom events capture, session replays, feature flags, and more.
@@ -37,6 +38,8 @@ See the [JavaScript SDK docs](/docs/libraries/js/features) for all usable functi
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Setting up PostHog on the server side
Install `posthog-node` using your package manager:

View File

@@ -6,6 +6,7 @@ icon: >-
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about usage of your [Nuxt.js](https://nuxt.com/) app. Integrating PostHog into your app enables analytics about user behavior, custom events capture, session replays, feature flags, and more.
@@ -93,6 +94,8 @@ export default defineEventHandler(async (event) => {
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Error tracking
For a detailed error tracking installation guide, including automatic exception capture and source map configuration, see the [Nuxt error tracking installation docs](/docs/error-tracking/installation/nuxt).

View File

@@ -19,6 +19,7 @@ features:
import DetailSetUpReverseProxy from "../../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../../integrate/_snippets/details/posthog-ips.mdx"
## Installation
@@ -30,6 +31,8 @@ import ReactNativeInstall from '../../integrate/_snippets/install-react-native.m
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
### Configuration options
You can further customize how PostHog works through its configuration on initialization.

View File

@@ -20,6 +20,7 @@ features:
import DetailSetUpReverseProxy from "../../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about traffic and usage of your React app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
@@ -39,6 +40,8 @@ import ReactInstall from '../../integrate/_snippets/install-react.mdx'
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Usage
### PostHog provider

View File

@@ -6,6 +6,7 @@ icon: >-
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about traffic and usage of your [Remix](https://remix.run/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
@@ -109,6 +110,8 @@ When you run your app now, PostHog will automatically capture events and pagevie
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Next steps
For any technical questions for how to integrate specific PostHog features into Remix (such as analytics, feature flags, A/B testing, surveys, etc.), have a look at our [JavaScript Web SDK docs](/docs/libraries/js/features) and [React](/docs/libraries/react) docs.

View File

@@ -6,6 +6,7 @@ icon: >-
import DetailSetUpReverseProxy from "../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about traffic and usage of your [Svelte](https://svelte.dev/) app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.
@@ -27,6 +28,8 @@ import SvelteInstallClient from "../integrate/_snippets/install-svelte-client.md
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Server-side setup
import SvelteInstallServer from "../integrate/_snippets/install-svelte-server.mdx"

View File

@@ -6,6 +6,7 @@ icon: >-
import DetailSetUpReverseProxy from "../../integrate/_snippets/details/set-up-reverse-proxy.mdx"
import DetailGroupProductsInOneProject from "../../integrate/_snippets/details/group-products-in-one-project.mdx"
import DetailPostHogIPs from "../../integrate/_snippets/details/posthog-ips.mdx"
PostHog makes it easy to get data about usage of your [Vue.js](https://vuejs.org/) app. Integrating PostHog into your app enables analytics about user behavior, custom events capture, session replays, feature flags, and more.
@@ -55,6 +56,8 @@ import PluginEvents from "./_snippets/plugin-events.mdx"
<DetailGroupProductsInOneProject />
<DetailPostHogIPs />
## Other setup options
import ProvideInjectInstall from "./_snippets/provide-inject-install.mdx"

View File

@@ -23,7 +23,7 @@ You don't need any vendor specific SDKs just use standard OpenTelemetry libr
Install and configure your logging client to send logs to PostHog:
- Use the HTTP endpoint: `https://us.i.posthog.com/logs`
- Use the HTTP endpoint: `https://us.i.posthog.com/i/v1/logs`
- Authenticate with your project token (same token as events/exceptions)
- Include the token in the Authorization header or as a `?token=` query parameter
- Use the standard [OTLP log format](https://opentelemetry.io/docs/specs/otel/logs/data-model/)

View File

@@ -21,7 +21,7 @@ import Beta from './_snippets/beta.mdx'
**Problem**: Cannot connect to the PostHog logs endpoint.
**Solutions**:
- Verify the endpoint URL: `https://us.i.posthog.com/logs`
- Verify the endpoint URL: `https://us.i.posthog.com/i/v1/logs`
- Check that your application can make outbound HTTPS requests
- Ensure firewall rules allow outbound connections to PostHog
- For self-hosted instances, verify the endpoint is correct for your deployment

View File

@@ -21,6 +21,8 @@ export const dashboardfiltersDark = "https://res.cloudinary.com/dmukukwp6/video/
<iframe
src="https://www.youtube-nocookie.com/embed/2jQco8hEvTI?start=441"
className="rounded shadow-xl"
width="600"
height="337"
/>
Dashboards are the easiest way to track all your most important product and performance metrics.

View File

@@ -8,6 +8,8 @@ availability:
enterprise: full
---
import DetailPostHogIPs from "../integrate/_snippets/details/posthog-ips.mdx"
<iframe
src="https://www.youtube-nocookie.com/embed/2jQco8hEvTI?start=250"
className="rounded shadow-xl"
@@ -39,6 +41,8 @@ There are three kinds of heatmaps in the toolbar:
You can also view heatmaps directly in the PostHog. This method is currently in beta and provides almost the same functionality as the toolbar.
<DetailPostHogIPs />
To view heatmaps in the app:
1. Enable the [**In-App Heatmaps** feature preview](https://app.posthog.com/settings/user-feature-previews#heatmaps-ui)

View File

@@ -44,13 +44,6 @@ Top paths drill down into specific pages on your site to show their views, visit
Entry and end paths show these stats for the first and last pageviews of each session, while outbound clicks shows the URLs that users clicked on to leave your site.
### Scroll depth
Both average scroll and deep scroll rate are calculated using how far a user has scrolled down the page and how much content has scrolled into view.
- **Average scroll depth** is the average scroll percentage across pageviews.
- **Deep scroll rate** is the percentage of users who scroll far enough down a page to view 80% of the content.
## Channels, referrers, UTMs
To get an idea of where users are visiting your site from, you can see top referrers, [channels](/docs/data/channel-type), and [UTMs](/docs/data/utm-segmentation).

View File

@@ -147,6 +147,7 @@ Loud Noises is used in the sign the hedgehog is holding:
If you have questions about which font to use, please ask in <PrivateLink url="https://posthog.slack.com/archives/C01V9AT7DK4">#team-brand-and-vibes</PrivateLink> - don't just do what feels right to you!
## Colors
We have two color schemes (light and dark mode), but primarily use light mode.
@@ -180,9 +181,23 @@ When possible, use opacity to modify colors. This allows us to use fewer colors
We use [Pitch](https://pitch.com) for polished presentations (like when giving a talk). Read more about this in our [communication guidelines](/handbook/company/communication#google-docs-and-slides).
## Illustration guide
Our hedgehog mascot is called Max and we're quite particular about how he (or any of his hoggy pals) are illustrated. We're exploring AI tools for internal use, but currently ask that you don't use AI tools to create your own hedgehog art. Instead, you can follow the guidelines below, or [create a new art request](/handbook/brand/art-requests).
![How to draw a hedgehog](https://res.cloudinary.com/dmukukwp6/image/upload/q_auto,f_auto/hog_guide_1_2fab7b9cb6.png)
If Max is drawn in color he should always have a beige body with brown spines, arms, and legs. His arms should only bend once in the middle and he doesn't have fingers unless swearing or pointing. His feet are stubby by design and his snout lines should be visible unless obscured by a mask or beard. His expression comes mainly from his eyebrows.
![Draw the rest of the hedgehog](https://res.cloudinary.com/dmukukwp6/image/upload/q_auto,f_auto/hog_guide2_d61482248f.png)
He should be outlined with a strong, black monoline with consistent thickness. He should always face left, right, or straight-on but shouldn't be drawn with a side profile or from behind as he's self-conscious.
> A more detailed version of this guide is <PrivateLink url="https://www.figma.com/file/I0VKEEjbkKUDSVzFus2Lpu/Hoggies?type=design&node-id=0-1&mode=design&t=H3ElmuzbLMFp4qP7-0">available on Figma</PrivateLink> for team members.
## Hedgehog library
For team members we keep all our currently approved hedgehogs [in this Figma file](https://www.figma.com/file/I0VKEEjbkKUDSVzFus2Lpu/Hoggies?type=design&node-id=0-1&mode=design&t=H3ElmuzbLMFp4qP7-0). This enables us to look through the library of approved hogs, and to export them at required sizes without relying on the design team.
For team members we keep all our currently approved hedgehogs <PrivateLink url="https://www.figma.com/file/I0VKEEjbkKUDSVzFus2Lpu/Hoggies?type=design&node-id=0-1&mode=design&t=H3ElmuzbLMFp4qP7-0">in this Figma file</PrivateLink>. This enables us to look through the library of approved hogs, and to export them at required sizes without relying on the design team.
Here's how:

View File

@@ -27,16 +27,18 @@ This guidance applies to all teams, irrespective of whether you manage an engine
### Part-time managers
Because of the relatively short list of tasks that managers have, management at PostHog is a part-time job. That means everyone, including the founders, still spend the majority of their time on practising what they do best - for most managers, this isn't actually management!
Because of the relatively short list of tasks that managers have, management at PostHog is a part-time job. That means nearly everyone still spends the majority of their time on practising what they do best. For most managers, this isn't actually management!
As an engineer, you wouldn't respect the opinion of someone who can't code on a coding-specific question.
As an engineer, you want the opinion of someone who can actually code.
As a designer, you really want your manager to have an eye for design.
As an operator, you want to be managed by someone who has scaled a business.
That's why it's important for managers to keep practising their craft.
However, management tasks do come _first_, as giving context to your team tends to have a multiplying effect vs. getting one more PR out. After that though, it's back to work.
> You'll sometimes hear us use the term "team lead". A team lead is the leader of a small team. By default they also manage the individuals that are part of their team, though very occasionally they don't, such as when a new small team has just been created.
Management is intentionally spread thin at PostHog. This is a forcing function for making sure that teams and ICs continue to have high levels of autonomy. Bored managers are micromanagers. By working across several teams, people like #team-blitzscale and product managers are forced to only give their attention where it's truly needed, and give space & autonomy everywhere else.
> You'll sometimes hear us use the term "team lead". A team lead is the leader of a small team. By default they also manage the individuals that are part of their team, though very occasionally they don't, such as when a new small team has just been created.
## How do I set context?
@@ -139,6 +141,8 @@ As such, management roles are paid on the same pay scale as other ICs. Becoming
Management is a skill of its own, and it's not any more important than any other skills that make someone a great IC. It's possible that you may be a manager for a short time, but it becomes clear that your strengths lie primarily in the other skills that are involved with being an IC. In this case we might move you back to a pure IC position, where your skills can really shine, and move someone else from your team or from around the company into the manager / team lead role.
Additionally, managers who are excelling with their teams may have limited interaction with their own manager. This is because, as discussed [above](#part-time-managers), management is intentionally spread thin. If you feel like your manager is mostly ignoring you, this isn't necessarily a bad thing and usually means you and your team are doing a fine job!
## Recommended reading
These have been recommended by multiple managers on the team:

View File

@@ -59,7 +59,7 @@ Before going on call, make sure you have the **Incident.io mobile app** [Android
> TRICKY: We use Slack auth for incident.io and Slack really doesn't like you using the mobile web version. Make sure to choose `Sign in with Slack` and then use your email to login to Slack, **not** google auth as that seems to cause redirect issues for some people.
> Still having redirect issues signing up with Slack? Create a Slack password instead of using Google SSO, then log in with that password.
> Still having redirect issues signing up with Slack? [Create a Slack password instead of using Google SSO](https://posthog.slack.com/account/settings#password), then log in with that password.
To get a calendar with all your on-call shifts from incident.io go to the [schedules section](https://app.incident.io/posthog/on-call/schedules), select `Sync calendar` at the top right and copy the link for the webcal feed. In google calendar, add a new calendar from URL and paste the link in there.

View File

@@ -2,6 +2,8 @@
title: Managing cool tech jobs
---
> **Applying to get your jobs listed?** The Cool Tech Jobs board exists to help people find jobs at companies with similar perks and culture to PostHog, and a strong engineer-led environment. Applications are approved only at our discretion and moderation can take up to 48 hours. If you have a question about an application, please [contact our support team](http://app.posthog.com/home#supportModal).
## Create a company/jobs:
### Non-moderator flow

View File

@@ -26,6 +26,8 @@ The example below shows a presentation that is personalized for a particular com
- Supports some variables like `{companyName}` and `{companyLogo}` that can be used within slides
- Connects with Salesforce to personalize the presentation with the assigned rep
- Embeds a Default.com scheduling form
- Supports a lead form
- Can optionally hide slide thumbnails and presenter notes
- And yes, they're fully responsive, and if an uploaded screenshot has a dark mode equivalent, it will be used in dark mode
## How it works
@@ -83,6 +85,12 @@ Here's a general structure for a presentation using the various templates. You c
```
{
"name": "Product Engineers",
"config": {
"thumbnails": false,
"notes": false,
"form": true,
"teamSlug": "sales-cs"
},
"slides": {
"overview": {
"template": "stacked",
@@ -247,7 +255,9 @@ This is a multi-column layout that supports multiple products or features side-b
}
```
## Overriding default content
## Customization
### Overriding default content
You can create an entirely personalized presentation, use the `dream-customers` folder. Set the json filename to the company's domain name and inside the file, set an arbitrary ID that will be used in the URL, like:
@@ -272,3 +282,66 @@ Reference content from any persona file with `inherit`, or override the content
"slideKey": "feature_flags"
},
```
### Display options
Use the `config` object in the JSON file that supplies the content for the presentation to customize how the presentation renders. This can be done for a persona, a specific company, or an individual.
#### All properties
```
"config": {
"thumbnails": false, // hides slide thumbnails column
"notes": false, // hides presenter notes drawer
"form": true, // shows the lead form
"teamSlug": "sales-cs" // specifies which Small Team appears in the form
}
```
The `thumbnails`, `notes`, and `form` values can be overridden in the query string (independently), like:
```
/for/product-engineers?thumbnails=false&notes=false&form=true
```
These configuration options are remembered when using the _Share your windows_ link generator in the _Active windows_ pane. This is useful for sending a link to someone that will open multiple windows _and also_ remember the display options of a presentation.
### Lead form
The lead form is hidden by default but can be enabled in a persona's JSON file, or displayed manually using the query param `&form=true`.
```
"config": {
"form": true,
}
```
Non-personalized (industry-specific) landing pages show avatars of the <SmallTeam slug="sales-product-led" />.
![Default small team](https://res.cloudinary.com/dmukukwp6/image/upload/w_800,c_limit,q_auto,f_auto/pasted_image_2025_11_06_T22_54_26_064_Z_ac2d292639.png)
Company-specific landing pages show the <SmallTeam slug="sales-cs" /> by default.
![No assignment in Salesforce](https://res.cloudinary.com/dmukukwp6/image/upload/w_800,c_limit,q_auto,f_auto/pasted_image_2025_11_06_T22_31_35_421_Z_67d9d50a39.png)
This is because different URL patterns are intended for different purposes.
| Path | Purpose | Team |
| -------------------------- | -------- | ----------------------- |
| `/for/{company}/{persona}` | Outbound | New Business Sales Team |
| `/for/{persona}` | Inbound | Product-Led Sales Team |
### Small team customization
The small team in the `config` object can be overridden for any persona, company, persona within a specific company, or completely custom landing page.
```
"config": {
"form": true,
"teamSlug": "sales-cs"
}
```
On landing pages personalized to a specific company, we check if the account is assigned in Salesforce. This takes priority over any small team assignment in JSON.
![Account assigned in Salesforce](https://res.cloudinary.com/dmukukwp6/image/upload/w_800,c_limit,q_auto,f_auto/pasted_image_2025_11_06_T22_27_14_309_Z_58d38f7173.png)

View File

@@ -40,7 +40,9 @@ To help the new owner of a customer hit the ground running, we should make sure
- TAE -> TAM 1-3 months after the initial contract is signed and the customer is onboarded into 1 or 2 primary products.
- TAE -> CSM 1-3 months after the initial contract is signed and the customer is onboarded into 3 or more primary products.
- TAE -> CSM after sufficient cross-product expansion has take place.
- TAM -> CSM after sufficient cross-product expansion has take place.
> For accounts who will be landing at $100k+ a year or have high expansion potential after the initial contract, we should involve a TAM early in the process to ensure a smooth transition. See the section further down this page on how this works.
For handover to take place there should be an Account Plan (saved as a note on the account in Vitally) and the customer should have been onboarded properly to the products they are currently paying for.
@@ -93,3 +95,13 @@ Before handing over a customer, the existing owner needs to ensure that the cust
#### Error Tracking
- They have set up tracking using posthog-js.
## High potential customers
For TAE-led customers who will be landing at $100k+ a year or have high expansion potential into new product areas, we should introduce a TAM earlier on than normal.
The prime time for this is when the technical win is confirmed - the TAM should be introduced to the customer by the TAE in an evaluation or POC wrap-up call when we know that the customer has selected PostHog.
The introduction is purely for relationship building and continuity purposes so that the TAM can hit the ground running with the customer after the initial contract is signed. It's still on the TAE to work with the customer on contracts etc, and as such only the TAE will be recognized on the initial contract for commission purposes. After the initial deal is closed won the TAM will take over the account in their book of business.
The TAE and TAM should use their overlapping time to work with the customer on a documented onboarding plan per the above guidance.

View File

@@ -0,0 +1,9 @@
---
title: Communications templates
sidebar: Handbook
showTitle: true
---
We've put together suggested communications templates that TAMs can draw on for various situations like startup plan roll off, incidents, churn risk increase, or new feature rollouts.
These templates are not meant to be restrictive, but a general idea of how we communicate with customers.

View File

@@ -127,13 +127,13 @@ You might see some customers with a 30% discount on their monthly Stripe subscri
### Startup plan discounts
For customers on our [startup plan](/startups), we offer two months free credit when signing a prepaid deal. This encourages startups to use their credits to understand usage, and then commit to a longer term plan with PostHog. This offer is available until the first billing date after the credits expire. If a customer has used up their credits before the expiration date, they still have until the original expiration date to decide and claim the offer. The amount of free credits is determined by how much they purchase on a prepaid plan. By default, we work with customers on prepaid plans that will cover their usage for the next 12 months.
For customers on our [startup plan](/startups), we offer two months free credit when signing a prepaid deal. This encourages startups to use their credits to understand usage, and then commit to a longer term plan with PostHog. This offer is available until the first billing date after the credits expire. If a customer has used up their credits before the expiration date, they still have until the original expiration date to decide and claim the offer. The amount of free credits is determined by how much they purchase on a prepaid plan. By default, we work with customers on prepaid plans that will cover their usage for the next 12 months.
You should follow the same [inbound sales process](https://posthog.com/handbook/growth/sales/new-sales) and work with the customer on understanding and optimizing their usage. Then follow these additional steps take to present the prepaid plan + free credits option(s):
1. Review the customer's average monthly cost
2. Estimate the prepaid equivalent for 12 months of coverage (e.g. [monthly cost x 12])
3. Inform them they can take advantage of this offer, which gives them the option to purchase [monthly cost x 10] and still have 12-months of coverage.
4. Check whether buying [monthly cost x 10] gives them a lower [discount rate](https://posthog.com/handbook/growth/sales/contract-rules#discounts)
4. Check whether buying [monthly cost x 10] gives them a lower [discount rate](https://posthog.com/handbook/growth/sales/contract-rules#discounts)
5. If so, you should ALSO present the option to buy [monthly cost x12], and they'll receive [monthly cost x14] AND take advantage of the higher discount.
6. If the customer wants to purchase fewer credits than either option above, then they will receive an additional 1/6 of the amount they wish to purchase for free.
@@ -143,6 +143,18 @@ All free credits associated with startup plan roll-offs are one-time only, and s
As it's often difficult to right-size the credit needed for a longer term plan as a standard we offer to honor the discount provided in the original purchase for any additional credit purchased in the first half of a contract term (e.g. 6 months for an annual plan). Within the first 6 months given our billing usage reports we should be able to predict whether the customer is going to run out of credit or not. There are also alerts set up in #sales-alerts to help notify account owners about this.
## Price Guarantees & Lock-ins
Our default stance is to not offer price guarantees for the following reasons:
1. We regularly *lower* prices, which would result in higher costs for customers who've locked in a price
2. We occasionally split or restructure products (e.g. Data Pipelines unbundled), which makes guarantees administratively complex
3. Customers are in full control of their usage and can thus adjust their spending patterns as needed
This request most often comes from procurement teams unfamiliar with our pricing philosophy. Address it proactively in commercial discussions, but if there is push back, reference the above points. As an example:
> "We've dropped Events pricing [X]% over [timeframe]. A price guarantee would have cost you more. We're committed to being the cheapest at every scale—if we're not, tell us. Our prepaid credits for usage based pricing gives budget control without betting against our commitment to low prices."
## Multi-year credit allocation
### Paid up-front

View File

@@ -8,9 +8,9 @@ showTitle: true
We use [Salesforce](https://posthog.lightning.force.com/lightning/page/home) as our customer relationship management ('CRM') platform. If you need access, you can ask <TeamMember name="Mine Kansu" photo /> for an invite.
As a first step, you might find it useful to install Salesforce's [Chrome extension](https://chromewebstore.google.com/detail/Salesforce/jjghhkepijgakdammjldcbnjehfkfmha?hl=en-US), as it means you can manage most things directly in Gmail.
As a first step, make sure you connect your Gmail account under your Salesforce settings. Go to Settings → Connected Accounts → Gmail and connect it. This ensures all your customer emails sync automatically with Salesforce. This is essential so that we capture the full customer context and avoid duplicate or conflicting outreach.
As a general principle, we try to ensure as much customer communication as possible is captured in Salesforce, rather than in individual email inboxes, so that we make sure our users are getting a great experience (and not confusing or duplicate messages from different team members!). You should use the channel that suits the user, not us. Just make sure you keep Salesforce up to date with your interactions. We've seen much higher response rates on Slack than email.
As a general principle, we try to make sure as much customer communication as possible is captured in Salesforce rather than in individual email inboxes so that we make sure our users are getting a great experience (and not confusing or duplicate messages from different team members!). You should use the channel that suits the user, not us. Just make sure you keep Salesforce up to date with your interactions. Weve found Slack messages usually get better response rates than email.
For existing customers, you'll sometimes send emails directly from <PrivateLink url="https://posthog.vitally-eu.io/">Vitally</PrivateLink>. To ensure these also make it to Salesforce, first look up your _Email to Salesforce Address_ from the [personal settings page](https://posthog.lightning.force.com/lightning/settings/personal/EmailToSalesforceUserSetup/home) in Salesforce, and then add it to your <PrivateLink url="https://posthog.vitally-eu.io/settings/profile/gmail">Vitally gmail settings</PrivateLink>.
@@ -18,7 +18,7 @@ All Slack messages sync up with the corresponding account in Salesforce. We use
You are most likely to use the following regularly:
- _Leads_ - A lead is a potential customer who has shown interest but hasn't yet been qualified. We create leads for every new user emailing sales@posthog.com or filling out contact sales form on our website. You can also create them manually if you are introduced through other sources (e.g. events, referrals) or by tagging tickets in Zendesk.
- _Tasks_ A task represents a potential sales follow up or engagement. Every new inbound inquiry (via form or email) is now created as a task on an account and contact.
- _Opportunities_ - An opportunity is a qualified lead that has been assessed and is considered a potential sales deal (with an estimated revenue and an expected close date). This is where we manage our customers through their buying cycle.
- _Contacts_ - Contacts are individuals who use PostHog or contacts we interact with. You can create contacts manually or convert a Lead to a Contact after evaluating the lead and deciding to continue working with them.
- _Accounts_ - You will also create an account record to associate with any contact. You can associate multiple contacts with a single account. If you enter the company's domain name, we have data enrichment in place to pull in additional data on the organization.
@@ -29,22 +29,14 @@ Salesforce offers a ton of [resources](https://trailhead.Salesforce.com) if you
People currently come into Salesforce through one of the following ways:
- Email Inquiries: When someone emails our sales team at sales@posthog.com
- Website Forms: When they complete a contact or demo request form on our website
- Product Sign-ups: When they sign up for specific products or plans that include onboarding assistance (e.g., Teams Plan)
- Email inquiries: messages sent to sales@posthog.com
- Website forms: when they complete a contact or demo request form on our website
- Product sign ups: All signups are saved as a contact record in Salesforce
- Manual Entry: When a team member manually adds a contact, such as meeting someone interested in PostHog at an event
### Email
We respond to emails which come into sales@posthog.com by replying with sales@ in BCC to ensure that everyone else on the team knows that you're handling the query.
If you've turned on the Salesforce Chrome Extension you can see the person's Salesforce profile directly within Gmail which should give you their auto-computed [Lead score](/handbook/growth/sales/lead-scoring) so this will help you decide on the correct approach (hands-on/self-serve).
We have lots of handy templates you can use as well - just select _Template_ in the email window in Salesforce. If you find yourself sending the same type of email repeatedly, you may want to create your own template - go to 'App Launcher' (the grid icon) > 'Email Templates' > 'New Email Template'.
### New PostHog signups
When a `user signed up` (Cloud signup) or `license purchased` (Self-host license purchase) event is ingested into PostHog
When a `user signed up` (Cloud signup) event is ingested into PostHog
we use the [Salesforce App](https://github.com/PostHog/Salesforce-plugin) to sync contact data into Salesforce. We also populate
the following Salesforce properties if they are set in the PostHog event:
@@ -55,18 +47,14 @@ the following Salesforce properties if they are set in the PostHog event:
### Completed contact form
We have a [contact us form](/talk-to-a-human) on posthog.com where we ask users can get in touch with us. The sales@ alias gets an email notification and a notification is also sent to [#website-contact-sales](https://posthog.slack.com/archives/C054BJSHG82) in Slack when one of these forms is submitted - respond to them in the same way as the email section above.
We have a [contact us form](/talk-to-a-human) on posthog.com where we ask users can get in touch with us. The sales@ alias gets an email notification and a notification is also sent to [#sales-leads](https://posthog.slack.com/archives/C054BJSHG82) in Slack when one of these forms is submitted.
> If you receive a lead where someone completes the contact form but it is clearly just a regular support request, you should add :ticket: to the relevant thread in the `#website-contact-sales` channel in Slack and mark it as "Unqualified" in Salesforce.
These submissions are processed through the Default app and routed into Salesforce as tasks. Tasks are then automatically assigned to the right team member based on account ownership and territory (see below).
### Manually adding new records
You can also just manually add a user or an organization to Salesforce if they come through different channels. When creating a new contact, try to add as much useful information as possible, especially about the type of company they work for and what their needs are. This enables us to provide them with the best possible experience.
Our preferred way to keep track of outreach is by creating Salesforce Leads, Contacts, and Opportunities, depending on the stage of the relationship:
- Leads: If a contact is in the initial evaluation stage, it should be entered as a Lead. This allows us to track and manage potential clients who are not yet qualified or who are still in the early stages of engagement. This is typically how we add potential customers who do not use PostHog yet.
- Opportunities: If you identify potential for growth with an existing user, such as expansion or commitment to an annual plan, you can create an Opportunity directly. This is appropriate for contacts or customers where you've already completed the lead assessment (more on that below).
If the submission is clearly a support or billing request, you dont need to reach out manually:
- Select the Disqualification reason “Billing Support Request” or “Support Request.”
- This automatically creates a Zendesk ticket for the correct team.
- No manual outreach is needed, automation handles it.
### Zendesk Integration
@@ -80,79 +68,37 @@ If you are not in the sales team but are engaged with a client and identify a sa
**Important:** The email must be forwarded (not replied to), and sales@posthog.com must be in the "To:" field—not CC or BCC—for the automation to work correctly.
## How we do lead assignments
## Task assignment logic
Any user who submits a “contact sales” form on our website shows up as a lead in Salesforce and gets assigned to an Account Executive (AE). This is how we do lead assignment within our sales team:
When a new task is created, we first check whether the associated account already has an owner:
- Based on Territory: Leads are assigned based on their geographical location.
- If the account has an owner, task is automatically assigned to that person.
- If the account is unowned, account and task are assigned to a salesperson via round robin within their territory.
- Territory 1: Users based in the US, Canada, Central and Latin America are assigned to this territory.
- Territory 2: Users based in Europe, the Middle East, Australia, and Africa are assigned to this territory.
- Territory 3: All other geographies, or if the country information is missing, assigned to this territory.
This ensures we avoid double assignments and maintain clear ownership.
- Round Robin Within Territory: Leads are assigned in a round-robin fashion to the AEs who own that territory, ensuring equal distribution among them.
- Reassignment Logic for Returning Leads: If a lead incoming from a specific domain later submits another contact sales form, the new lead is reassigned to the original owner if the old lead came in within the last 4 months. This ensures continuity in discussions with different leads from same company while preventing spam leads from being reassigned to the original owner.
Territories
- U.S. West
- U.S. East
- Europe & Africa
- Asia & Middle East
- Australia & New Zealand (ANZ)
### Lead pool process (experimental)
Each territory runs its own round robin assignment for new, unowned accounts.
Were testing a **new process** for handling **product-led leads** that havent been acted on within **7 days**. If a lead is assigned to an AE but hasnt been updated for **7 days**, it will:
### Converting tasks to opportunities
1. Automatically unassign from the AE.
2. Move to a shared lead pool where anyone can pick it up and take action.
If a task represents a qualified opportunity:
- Open the task and check the box labeled “Create new opportunity.”
- Choose the appropriate Opportunity record type:
- New Revenue for brand-new customers.
- New Revenue Existing Customer for upsells, cross-sells, or expansions.
- Existing Convert to Annual for pay as you go customers moving to an annual plan.
- Renewal for existing annual customers renewing their plan.
- This automatically creates and links the opportunity to the task.
- You can then click the opportunity link to add deal details (value, close date, etc.)
#### Why are we doing this?
- **Keeps leads active:** Ensures leads dont go stale and slip through the cracks.
- **Encourages responsiveness:** AEs are motivated to act quickly to keep their assigned leads.
- **Creates shared accountability:** Gives others a chance to work leads that need attention.
- **Helps identify patterns:** Tracks unqualified leads and informs adjustments to our lead routing.
#### What you should do
- **Prioritize your assigned leads** to avoid them being reassigned. ✅
- **Check the lead pool regularly**—there might be great leads waiting for you to pick up. 🚀
- **Mark unqualified leads** in Salesforce instead of leaving them in the pool. ❌ Flagging them helps us track trends and improve lead quality.
#### What _not_ to do
- **Ignore assigned leads.** Leads that sit untouched will be unassigned and go to the pool.
- **Dump bad leads into the pool.** If youve reviewed a lead and its not a fit, mark it appropriately in Salesforce.
### How we assess leads in our pipeline
We have the following lead statuses to manage the lead assessment process before we decide if a user is a right fit to use PostHog.
New: A lead that has just been entered into Salesforce and has not yet been contacted.
- Review lead details and ensure all necessary information is captured.
- Perform an initial qualification check based on lead information.
Working: A lead that you are actively engaging with.
- Reach out to the lead via email, schedule a meeting if theyre interested.
- Gather additional information to further qualify the lead.
- Update lead details with any new information obtained.
- Assess lead needs and match with PostHog solutions.
Nurturing: A lead that requires further development before they are ready to make a purchasing decision (e.g. if they said lets chat again in 3 months).
- Schedule follow-up activities (e.g., calls, emails, meetings).
- Provide valuable content (e.g., feature updates, product launches, blogs) to build the relationship.
- Monitor engagement with the content and interactions.
Converted: A lead that has been qualified and is ready to become an opportunity.
- Convert the lead to an account, contact, and opportunity in Salesforce.
- Ensure all relevant information is transferred accurately and opportunity type is selected properly (more on that below).
Unqualified: A lead that does not meet the criteria to become an opportunity.
- Document the reasons for disqualification in the “Disqualification reason” field in Salesforce (e.g., budget constraints, lack of fit, self serve customer, non-opportunity-related inquiries, support requests).
- Update the lead status to 'Unqualified'.
### Lead to opportunity conversion guidelines
Use the following criteria (loosely based on traditional BANT qualification) to determine when a lead should be converted to an opportunity:
Use the following criteria (loosely based on traditional BANT qualification) to determine when a task should be converted to an opportunity:
- You've had at least one call with the customer to establish a relationship.
- There's a clearly identified problem that PostHog can solve.
@@ -170,9 +116,16 @@ All of the above criteria should be met before creating an opportunity. By doin
If you aren't able to confidently say that you have covered the above, you should keep them as a Lead in the Nurturing stage.
### Manual entry
If you meet a potential customer elsewhere (e.g., events, introductions, referrals):
- Create the Account and Contact manually.
- Assign the correct Lead Source from drop down.
- Create a Lead Task for any action item or sales follow up.
### Support requests
If you receive a lead for a self-serve customer who has used the Sales Contact Form to submit a support request, you should:
If you receive a lead for a self serve customer who has used the Sales Contact Form to submit a support request, you should:
- Set the 'Disqualification reason' to 'Support Request'
- Update the lead status to 'Unqualified'
@@ -196,13 +149,10 @@ These mostly come into the sales inbox rather than the contact form. Whilst ther
- Keep all lead information up-to-date and accurate in Salesforce.
- Periodically review lead statuses and update them as needed.
### Which leads should go to RevOps?
Some incoming leads are better suited for RevOps, for example questions related to refunds, invoices, startup plan credits or eligibility. To ensure these leads are routed correctly, please create a zendesk ticket by leaving the :ticket: emoji in the relevant thread in the #website-contact-sales Slack channel. This will automatically create a ticket for RevOps to review and address.
## Opportunities
Opportunities track potential deals in Salesforce. Managing opportunities effectively is important for tracking progress, forecasting revenue, and ensuring accurate reporting. In our sales process, for each lead conversion we create a Contact, an Account, and an Opportunity. Correctly identifying the appropriate opportunity record type is important to optimize our processes.
Opportunities track potential deals in Salesforce. Managing opportunities effectively is important for tracking progress, forecasting revenue, and ensuring accurate reporting. In our sales process, for each lead conversion we create an Opportunity. Correctly identifying the appropriate opportunity record type is important to optimize our processes.
### Opportunity record types
@@ -222,13 +172,11 @@ Monthly Plan: Choose this type when the customer opts for a month-to-month subsc
### How to create an opportunity
#### Convert a lead
#### Convert a task
When converting a lead, Salesforce will prompt you to create a Contact, an Account, and an Opportunity. Under Opportunity:
If you're working a lead and want to create an opportunity from a task, simply check the Create New Opp checkbox and select the appropriate Opportunity Record Type from the dropdown.
- Select New Opportunity
- Choose the correct Opportunity Record Type from the dropdown menu
- Complete Opportunity Details: Ensure all mandatory fields are completed, including Name, Type, Close Date, and Amount.
This ensures the Lead Source is correctly carried over to the new Opportunity, and the task and opportunity remain linked for full visibility.
#### Creating an opportunity from scratch
@@ -239,7 +187,12 @@ You can also create an opportunity directly from scratch, but make sure to conne
- Fill in Opportunity Details:
- Opportunity Name
- Close Date: Choose the estimated date when the opportunity is expected to close.
- Amount: Enter the potential revenue amount for the opportunity (if blank this will be $20,000 by default). This should be the amount before any discounts are applied.
- Term (Months): Default is 12, update for multi year deals.
- Total Credit Amount: Total value of the contract before discounts.
- Discount (%): Percent discount applied to the total.
- ARR Discounted: Automatically calculated annualized revenue after discount.
- Contract Start Date: Date the contract begins.
- Contract End Date: Automatically calculated based on Start Date + Term.
- Stage: Select the current stage of the opportunity in the sales process.
- Type: If you know whether they're interested in paying on a monthly or an annual basis (if blank this will be Monthly by default)
- Connect to an Account: In the "Account Name" field, search for and select the account associated with the opportunity. If the account does not exist, create a new account first.

View File

@@ -1,5 +1,5 @@
---
title: Cross sell motions
title: Cross selling tactics
sidebar: Handbook
showTitle: true
---
@@ -21,4 +21,4 @@ Here's a collection of cross sell motions for specific products that can help yo
3. Create dashboards of the new error data that correlates errors with drop-offs in [conversion events](/docs/data/actions) (signups, checkouts, whatever is relevant here)
4. Share your analysis as a Loom or other low time commitment format for review, emphasising the uplift in conversion events if these errors were prioritised. If required present these findings back to the stakeholders.
5. If required help your stakeholder build a business case for the additional spend by linking the missed conversion events to value. For example, if the average LTV of a signed up user is 50$, multiply the dropoff in sign up events by 50 to get a rough ROI of finding and fixing these errors.
6. Pitch this value as a reason to remove the billing limit and expand usage of error tracking.
6. Pitch this value as a reason to remove the billing limit and expand usage of error tracking.

View File

@@ -0,0 +1,72 @@
---
title: Customer case studies
sidebar: Handbook
showTitle: true
---
### What makes a good case study?
Case studies should make our users look smart, our products look useful, and PostHog look like a company people actually want to talk to.
**Things we don't care about:**
- if they pay us or not (most customers don't)
- if they use every tool in the box (they might be a power user of only one product)
- if they have a recognizable brand (big logos are nice, but more frequent, smaller stories often beat enterprise red tape)
**Things we do care about:**
- that PostHog has helped them achieve meaningful results
- that they represent [who we build for](/handbook/who-we-build-for)
- that someone else might benefit from reading their story
> Case studies are typically owned by the [marketing team](/teams/marketing). They live in `/contents/customers/` and appear on [posthog.com/customers](/customers). If you have a suggestion for who we should interview, let us know in the <PrivateLink url="https://posthog.slack.com/archives/C08CG24E3SR">marketing channel</PrivateLink>.
## Creating a case study
### 1. Identify the right customer
Start by asking the PM for that product. PMs do lots of user interviews and can suggest warm leads. You can also post in company Slack channels, but give some context for what you're looking for.
### 2. Make contact
Got a lead? Before reaching out, search for the company in Vitally. If they already have an assigned Account Executive or CSM, give that person a heads-up — they might already be working on something with the customer or have extra context on what to ask them about.
If theres no one assigned in Vitally, youre clear to go ahead and reach out directly.
Some customers have a dedicated Slack channel. If they do, thats usually the fastest way to coordinate. Otherwise, send an email.
### 3. Lay the groundwork
Someone agreed to chat? Hooray! Make a GitHub issue to draft some questions, tag any relevant sales/CS people, and note if youll need artwork later.
### 4. Schedule the interview
Who you talk to for interviews doesnt really matter. Speak to engineers, founders, PMs, or anyone who seems keen to chat. If youre unsure who to interview, email a few people at the company and see who bites.
>We use Calendly for scheduling external meetings, such as demos or product feedback calls. If you need an account, ask [Charles](https://posthog.com/community/profiles/28625) to invite you to the PostHog team account.
**How to be a good interviewer:**
1. Do some preliminary fact-finding (don't waste time asking general info about the interviewee's company and role)
2. Come prepared with [good, open-ended questions](https://posthog.com/newsletter/talk-to-users#:~:text=email%20with%20Substack-,4.%20What%20to%20ask%20during%20a%20user%20interview,-Our%20experience%20talking)
3. Relax and have a nice chat (30 minutes is plenty)
### 5. After the call
Trust your gut — if it feels like a good story, it probably is. Worst case, its still user feedback to pass on to other [small teams](/teams).
If it is worth turning into a case study, draft a PR right away while it's still fresh. Ask at least one teammate to review it to catch any grammar mistakes (or really bad jokes).
**Best practices:**
- **Be specific** - Use real numbers and measurable outcomes where possible
- **Use quotes** - Let the customer's voice come through
- **Keep it concise** - Aim for between 700-1400 words including quotes
### 6. Review and approval
PR looking good? Tag the customer in Github for review. You're not asking for copy edits just a quick fact check. Legal and PR teams will sometimes want to be looped in for approval as well. They might also request using Google Docs instead of GitHub.
Do what you need to do. The goal is to get the rubber stamp.
>If your draft might include anything private such as screenshots of customer dashboards, keep it in an internal repo like <PrivateLink url="https://github.com/PostHog/requests-for-comments-internal">requests-for-comments-internal</PrivateLink> just to be safe.
### 7. Publish!
Most people are excited to be featured and will sign off quickly. If you need artwork to go with the case study, use the [art or brand request template](https://github.com/PostHog/posthog.com/issues/new/choose)
Once the case study is merged and live on the website, the last step is to send a merch credit to the participants as thank you.
That's it - you did it!

View File

@@ -49,8 +49,8 @@ This has resulted in the highest number of qualified and motivated candidates re
'Ethiopia',
'Fiji',
'France',
'French Polynesia',
'Georgia',
'Germany',
'Iceland',
'India',
'Indonesia',
@@ -119,7 +119,6 @@ We are all-remote, but we have a few limitations on the countries we are able to
- Due to US sanctions, we can't hire folks in Cuba, Iran, North Korea, or Syria.
- We don't currently employ people via EOR in France, Italy, Sweden, Switzerland, Iceland, Belgium, Luxembourg, Uruguay, Bolivia, Denmark or Brazil, mainly due to the very high employer costs.
- In some of these countries we _may_ consider hiring as a contractor, provided there is no misclassification risk. We have done this before successfully in Brazil and Uruguay.
- We have an entity in Germany, which allows us to hire people directly. However, due to employment restrictions with companies with over 10 people, we have reached the total number of employees that we will hire here, so we will no longer be hiring in Germany and no current team member will be able to relocate to Germany.
## Hiring Process
@@ -194,7 +193,7 @@ Ashby also had a partnership with YC's job board so all roles to YC's [Work at a
Every time we open a new role, we will share the details and ideal profile with the team during All Hands.
#### What Qualifies as a Referral?
#### What qualifies as a referral?
A referral must meet these criteria to be eligible for a bonus:
@@ -202,7 +201,7 @@ A referral must meet these criteria to be eligible for a bonus:
- _Must provide valuable context_: Your referral should include meaningful insights beyond basic resume information
- _Be proactive_: Don't only wait for your network contacts to apply first - actively identify and reach out to potential candidates
#### Personal Referral
#### Personal referral
If you know someone who would be a great addition to the team, please submit them as a personal referral. If they're successfully hired, you'll receive a $2,500 referral bonus! The bonus can be either paid to you directly, or towards a charity of your choice where we will match the amount! You can also split the amount between you and the charity.
@@ -233,25 +232,25 @@ We recognize everyone is busy with limited bandwidth. If you'd like help identif
- Together we'll decide who makes the initial contact
- You'll still receive the referral bonus if they're hired
#### What's the Process?
#### What's the process?
If there is an ongoing conversation, please cc careers@ into the email thread with the referred candidate, and we will take it over from there. Otherwise, please upload the profile to the [Ashby referral page](https://app.ashbyhq.com/referrals).
**Important**: If they have applied themselves already, you cannot claim them as a referral - this includes candidates who applied weeks or months ago.
#### Social Referral
#### Social referral
You will sometimes get people emailing or messaging you on LinkedIn asking to chat about a role or get referred in. If you have a chat with them and think they are worth referring, but you dont know them enough to provide the talent team with valuable context, you can submit them as a social referral. If you don't know them, you can point them back to our [careers page](https://posthog.com/careers), or just ignore them. We get dozens of these kinds of messages every day, so don't feel bad about not engaging! If they are asking for advice, you can point them to [this article](https://posthog.com/newsletter/how-to-get-job-startup).
The referral bonus for social referrals is $500, and we again match any amount you choose to give this to charity.
If you are consistently posting about jobs to your networks and people apply after seeing those posts, those count as social referrals if they use your referral links. [Create your referral links in Ashby](https://app.ashbyhq.com/referrals/links) and you can use those when linking to job ads on LinkedIn or other platforms.
If you are consistently posting about jobs to your networks and people apply after seeing those posts, those count as social referrals if they use your referral links. [Create your referral links in Ashby](https://app.ashbyhq.com/referrals/links) and you can use those when linking to job ads on LinkedIn or other platforms.
#### Referral Payouts
#### Referral payouts
You'll get paid the bonus 3 months from the new team member's start date, and it will be processed as part of payroll. Bear in mind that you might be liable for income tax on the bonus.
#### Non-team Referrals
#### Non-team referrals
We also welcome external referrals, e.g. from:
@@ -328,7 +327,7 @@ This stage is usually a 20-minute video chat.
Candidates who are unsuccessful at this stage should receive a short personalized email with feedback.
### 2. Technical interview with the Hiring Manager
### 2. Technical interview with the hiring manager
In this round, the candidate will meet a future team member. This round is usually 45-60 minutes and will focus on a mix of experience and technical skills. Please check the specific hiring process for each team for more details.
@@ -336,7 +335,7 @@ As a rule of thumb, everyone interviewing must feel a genuine sense of excitemen
For engineering roles only: during high-volume seasons, this round might be recorded for training purposes to help us onboard and train new interviewers faster. The candidate will, of course, have the chance to opt out by either letting their recruiter know in advance or letting the interviewer know at the start of the interview.
### 3. Small Team interview with an Exec Team Member
### 3. Small Team interview with an Exec Team member
This is a call with either James, Tim, Raquel, or Charles depending on which Small Team they are being hired into. They will probe further on the candidate's motivation, as well as checking for alignment with PostHog's values.
@@ -348,7 +347,7 @@ The final stage of our interview process is what we call a PostHog SuperDay. Thi
> If it is difficult for a candidate to commit to a whole day in one go - they may not be able to get the time off, or have childcare commitments that make this difficult - we can be _very_ flexible. For example, we can split the SuperDay across two or more sessions, and can align timezones to suit the candidate, given we have a team that's globally distributed. A candidate will never lose out because they are not available to do a SuperDay right away.
The candidate will be working on a task that is similar to the day-to-day work someone in this role does at PostHog. They will also have the chance to meet a few of their potential direct team members, and if they havent already, our founders. This gives the candidate a chance to show off their skills, and for us to see the quality, speed and communication of the candidate.
The candidate will be working on a task that is similar to the day-to-day work someone in this role does at PostHog. They will also have the chance to meet a few of their potential direct team members, and if they havent already, our founders. This gives the candidate a chance to show off their skills, and for us to see the quality, speed and communication of the candidate.
As we grow, we find the need to hire engineers who are comfortable working with existing codebases to be increasingly fundamental. During SuperDay, Product Engineering candidates will also have a 45-minute debugging session. There is nothing to prepare in advance for this; you'll work with your interviewer in a pairing session to get through a bunch of bugs! It is a demanding day of work.

View File

@@ -25,32 +25,32 @@ Before you start, make sure that:
- You have authorized the PTO by Deel app in Slack to connect to your Google Calendar
- You have subscribed to the [team time off calendar](https://calendar.google.com/calendar/u/0/r?cid=c_52c05ff56171856873941d8a4e612c7d5dc317504b7533b0d22207480bc85763@group.calendar.google.com)
If you don't do this, your holiday won't show up in the team time off calendar.
If you don't do this, your holiday won't show up in the team time off calendar.
To book a day off:
- Book it on the PTO by Deel app in Slack. There are various types of time off you can select. It will be automatically approved and added to the team time off calendar (with the exception of longer term leave.) It will also be added to your manager's personal calendar.
- Do not book directly on deel.com as it does not sync with Slack, and the team will not know you are out. Yes, it is confusing that Deel have two separate systems here.
- Do not book directly on deel.com as it does not sync with Slack, and the team will not know you are out. Yes, it is confusing that Deel have two separate systems here.
- There are four types of time off you can select
- PTO - this will be the majority of the time for vacation or any other time off that doesn't fit below
- Out Sick - this is when you're sick and will take time off unexpectedly
- [Parental Leave](https://posthog.com/handbook/people/time-off#parental-leave)
- Medical Leave - this is planned time off for you personally, on medical grounds - if it is a family member who requires support meaning you will be off, this is PTO.
- Block out your own personal GCal to show that you are out. This is because PTO by Deel _only_ books in an all day event in your calendar to show that you are out. If you don't do this, automated meetings such as interviews or demos might still get booked into your cal.
- Medical Leave - this is planned time off for you personally, on medical grounds - if it is a family member who requires support meaning you will be off, this is PTO.
- Block out your own personal GCal to show that you are out. This is because PTO by Deel _only_ books in an all day event in your calendar to show that you are out. If you don't do this, automated meetings such as interviews or demos might still get booked into your cal.
- Set an out of office message on your email and have it point to someone else on the team, or hey@posthog.com. PTO by Deel will automatically set your Slack status to out of office and will autorespond to Slack messages.
> Please manually book in public holidays you plan to take off as well. We have team members working in countries all over the world, so it is not practical for us to book these all in on your behalf. Some people also prefer to work on certain days even if they're considered a public holiday in the country they are living in or visiting. In the Time Off by Deel app, you can use the Bulk Add by Region feature to quickly identify and add the public holidays you want off.
The same rules as above apply regardless of the holiday length and type. Sick leave and any other types of time off should also be booked in the same way.
The same rules as above apply regardless of the holiday length and type. Sick leave and any other types of time off should also be booked in the same way.
### How to cancel time off
If you decide to cancel your holiday, drop a message in #team-people-and-ops and a member of the team will cancel the holiday for you, as only admins can delete holidays.
If you decide to cancel your holiday, drop a message in #team-people-and-ops and a member of the team will cancel the holiday for you, as only admins can delete holidays.
## Flexible working
We operate on a trust basis and we don't count hours or days worked. We trust everyone to manage their own time.
We operate on a trust basis and we don't count hours or days worked. We trust everyone to manage their own time.
Whether you have an appointment with your doctor, school run with your kids, or you want to finish an hour early to meet friends or family - we don't mind and you don't need to tell us. Please just add it to your calendar and, if you are doing anything that could require you to be immediately available (ie support hero / or any customer-facing role), please make sure you have cover.
Whether you have an appointment with your doctor, school run with your kids, or you want to finish an hour early to meet friends or family - we don't mind and you don't need to tell us. Please just add it to your calendar and, if you are doing anything that could require you to be immediately available (ie support hero / or any customer-facing role), please make sure you have cover.
## When you should have time off
@@ -58,29 +58,29 @@ Whether you have an appointment with your doctor, school run with your kids, or
If you are sick, you don't need to work and you will be paid - the upper limit for paid sick leave for your country will be specified in your contract. This is assuming you need a day or two off, then just take them.
Please let your manager know if you need to take off due to illness as soon as you are able to and add it to PTO by Deel. You shouldn't pre-emptively book a bunch of days off sick, as you can't know how long you will actually be sick for and you may trigger the need for a doctor's note (see below). Just book the day or two off that you are sick then add more if you still feel unwell.
Please let your manager know if you need to take off due to illness as soon as you are able to and add it to PTO by Deel. You shouldn't pre-emptively book a bunch of days off sick, as you can't know how long you will actually be sick for and you may trigger the need for a doctor's note (see below). Just book the day or two off that you are sick then add more if you still feel unwell.
For extended periods of illness (5+ work days), or if you are going over the limit in your country/[state](https://support.gusto.com/article/106622152100000/Sick-leave-laws-by-state), please speak to Fraser so we can work out a plan. In most countries, we will need a doctor's note from you.
For extended periods of illness (5+ work days), or if you are going over the limit in your country/[state](https://support.gusto.com/article/106622152100000/Sick-leave-laws-by-state), please speak to Fraser so we can work out a plan. In most countries, we will need a doctor's note from you.
If you have a medical condition you know will take you away from work regularly, please let Fraser know so we can work out accommodations with you and your manager.
If you have a medical condition you know will take you away from work regularly, please let Fraser know so we can work out accommodations with you and your manager.
### Bereavements / Child loss
### Bereavements / Child loss
We do not define “closeness” and we won't ask about your relationship to the person or what they meant to you. Please just let us know up front how much time you would like to take.
We do not define “closeness” and we won't ask about your relationship to the person or what they meant to you. Please just let us know up front how much time you would like to take.
Our bereavement policy also covers pregnancy and child loss for both parents, with no questions asked. Please take at least 2 weeks of paid leave.
Our bereavement policy also covers pregnancy and child loss for both parents, with no questions asked. Please take at least 2 weeks of paid leave.
If you need extended time for physical or mental health reasons, we will treat it as extended sick leave - just chat to Fraser.
If you need extended time for physical or mental health reasons, we will treat it as extended sick leave - just chat to Fraser.
### Jury duty / voting / childcare disasters, aka 'life stuff'
There are lots of situations where life needs to come first. Please let it - just be communicative with your team and fit your work around it as you need. We trust you will do the right thing here.
There are lots of situations where life needs to come first. Please let it - just be communicative with your team and fit your work around it as you need. We trust you will do the right thing here.
If you are summonsed for jury duty, please let Fraser know right away - we can often get an exception granted if we have enough notice.
If you are summonsed for jury duty, please let Fraser know right away - we can often get an exception granted if we have enough notice.
## Parental leave
Parental leave is exceptional as it needs to be significantly longer than a typical vacation. Anyone at PostHog, regardless of gender, is able to take parental leave, and regardless of whether you've become a parent through childbirth or adoption.
Parental leave is exceptional as it needs to be significantly longer than a typical vacation. Anyone at PostHog, regardless of gender, is able to take parental leave, and regardless of whether you've become a parent through childbirth or adoption.
This table explains the amount of paid time off, depending on how long you've been at PostHog
@@ -90,13 +90,13 @@ This table explains the amount of paid time off, depending on how long you've be
| 6 - 12 months | 12 weeks | 2 - 3 weeks |
| over 12 months | up to 24 weeks | 6 weeks |
> Parental leave at PostHog is designed to be more generous than your local jurisdiction's legal requirements. This means that in most cases you will receive the PostHog policy, if you live in a country with more generous parental leave, then you will receive that. This PostHog policy is not designed to be _in addition_ to your specific state/country policy.
> Parental leave at PostHog is designed to be more generous than your local jurisdiction's legal requirements. This means that in most cases you will receive the PostHog policy, if you live in a country with more generous parental leave, then you will receive that. This PostHog policy is not designed to be _in addition_ to your specific state/country policy.
We only pay the enhanced parental leave in one continuous block.
Parental leave isn't supposed to be combined with our unlimited PTO policy here - we aren't prescriptive and will trust your judgement, but please note that we usually won't allow you do a combination of parental leave plus a long holiday in addition to that to extend your time off.
Parental leave isn't supposed to be combined with our unlimited PTO policy here - we aren't prescriptive and will trust your judgement. If you need a longer break after childbirth or a staggered return reach out to Fraser or your manager. But please note that we usually won't allow you do a combination of parental leave plus a long holiday in addition to that to extend your time off.
Please communicate parental leave to Fraser as soon as you feel comfortable doing so, and in any case at least 4 months before it will begin. They will let the People & Ops team know, who will follow up on any logistical arrangements around salary etc. and any statutory paperwork that needs doing.
Please communicate parental leave to Fraser as soon as you feel comfortable doing so, and in any case at least 4 months before it will begin. They will let the People & Ops team know, who will follow up on any logistical arrangements around salary etc. and any statutory paperwork that needs doing.
### Maternity leave
@@ -104,11 +104,11 @@ The above is in reference to Paid Time Off (PTO). Maternity leave can be extende
### Paternity leave
We do not offer unpaid leave for Paternity leave.
We do not offer unpaid leave for Paternity leave.
## Birthday and anniversaries
We celebrate all the big and little milestones at PostHog, including birthdays and work anniversaries. We celebrate each team member as a reminder of how much we appreciate them. Kendal is currently responsible for organizing these.
We celebrate all the big and little milestones at PostHog, including birthdays and work anniversaries. We celebrate each team member as a reminder of how much we appreciate them. Kendal is currently responsible for organizing these.
### Birthdays
@@ -119,35 +119,35 @@ These are the steps for making an order:
1. Log into our Wellbox account (details in 1Password)
2. Select the birthday gift to send
3. Fill out delivery information
4. All set!
4. All set!
The birthday gift usually arrives on the day of or 1-3 days prior to the birthday. Shipping fees: UK shipping is free while all other countries will have shipping fees.
The birthday gift usually arrives on the day of or 1-3 days prior to the birthday. Shipping fees: UK shipping is free while all other countries will have shipping fees.
### Anniversaries
On your first anniversary with PostHog, you will receive a giftcard from [Giftogram](https://giftogram.com/) or [Prezzee](https://www.prezzee.uk/business/signin/) (if you are based in the UK) which can be used on a wide selection of brands. On your second anniversary you'll be gifted a [customized Lego minifig](https://minifig.fab-bricks.com/) in a display case, and on your third anniversary, you'll receive a personalized gift from [Wellbox](https://wellbox.app/).
On your first anniversary with PostHog, you will receive a giftcard from [Giftogram](https://giftogram.com/) or [Prezzee](https://www.prezzee.uk/business/signin/) (if you are based in the UK) which can be used on a wide selection of brands. On your second anniversary you'll be gifted a [customized Lego minifig](https://minifig.fab-bricks.com/) in a display case, and on your third anniversary, you'll receive a personalized gift from [Wellbox](https://wellbox.app/).
#### 1st year anniversary
For the first year anniversary, we give $50 for US gift cards/$55 for all other countries gift cards to cover service fees:
1. Login into [Giftogram](https://app.giftogram.com/sign-in) by using your gmail credentials
2. Two ways to create a new Giftogram, on the tool bar above where it says “Create and Send'' or you can click on the right hand side on the blue button “Send a Giftogram''.
2. Two ways to create a new Giftogram, on the tool bar above where it says “Create and Send'' or you can click on the right hand side on the blue button “Send a Giftogram''.
3. Walk through the following steps:
- Select the appropriate campaign: US Campaign= US team members, GCode Campaign= EU+ALL team members, and CA Campaign= Canada team members
- Select a card design of your choice (easiest to just use the anniversary theme)
- Next screen, select “individual”, email as a delivery method, and add value (see above for amount) and continue to the next step
- Enter the individuals PostHog email address. You can add multiple email addresses if there is one then one anniversary. The amount will add itself on the right hand side as you add more individuals. Then, continue to the next step
- Delivery message; select PostHog team as the sender and select the drop down “1st year anniversary” as the pre-populated message or you can create your own personal message
- Last step, schedule the delivery date and youre done!
- Delivery message; select PostHog team as the sender and select the drop down “1st year anniversary” as the pre-populated message or you can create your own personal message
- Last step, schedule the delivery date and youre done!
#### 2nd year anniversary
The second year anniversary gets you a customized Lego figurine:
1. Log into [Fab-brick](https://fab-bricks.com/login.php) (login credentials are shared in People & Ops 1Password vault)
2. Select the third tab “MiniFig Creator” and design your mini fig to look like the individual youre celebrating!
2. Select the third tab “MiniFig Creator” and design your mini fig to look like the individual youre celebrating!
3. Make sure to include a display case and the three tier brick option
4. After youve completed your design, check out. There should already be a Brex card on file. Please make sure you add the individuals correct mailing address.
@@ -159,7 +159,7 @@ The third year anniversary is a pack of gifts provided via [Wellbox](https://wel
2. Fill out delivery info
3. You're all set!
The gift will usually arrive on the day of or 1-3 days prior to the anniversary date. Shipping fees: UK shipping is free while all other countries will have shipping fees.
The gift will usually arrive on the day of or 1-3 days prior to the anniversary date. Shipping fees: UK shipping is free while all other countries will have shipping fees.
#### 4th year anniversary

View File

@@ -8,4 +8,20 @@ hideAnchor: false
template: team
---
_COMING SOON_
## Who are we building for?
### Personas
- Primary Personas:
- **Product engineer**
- These are the engineers building the product. Normally full-stack engineers skewing frontend or frontend engineers.
- Product engineers have more limited time. Need to quickly get high-quality insights to inform what they are building and assess what they've shipped.
- **Product manager** (ex-engineer type)
- Supports the product teams (engineers, PMs, designers) to build the best products. They guide the product roadmap by speaking to customers and diving into the data.
- Product managers are the power-users of analytics (further evidence in the data analysis of paying users). They have desire and the time to go significantly deeper into the data.
- Limited focus:
- Growth engineer
- Not a focus but should be usable by:
- Everyone in the product team (less technical PMs, designers)
- Marketing
- Leadership team

View File

@@ -4,35 +4,30 @@
- **Rationale:** The more we polish, the shinier we get.
- **Things we could do:** Five year plan. Hire and onboard. Illustration guide.
- **Planning issue:** Coming soon?
- **We'll know we're successful when:** 80% of art is drafted before deadline.
### Make design 50% self-serve (Daniel H)
- **Rationale:** Self-serve enables more polish.
- **Things we could do:** Hog library. Production processes. Templates.
- **Planning issue:** Coming soon?
- **We'll know we're successful when:** Demand gen can self-serve ad designs.
### Make our parties bigger and better (Daniel Z)
- **Rationale:** Events work, so lets do more.
- **Things we could do:** More incubators. More outreach. Mulled wine.
- **Planning issue:** Coming soon?
- **We'll know we're successful when:** Our events start to become over-subscribed.
### Build the co-marketing playbook (Sara)
- **Rationale:** No more slipping through the cracks.
- **Things we could do:** Launch every CDP destination as a tier-1 product.
- **Planning issue:** Coming soon?
- **We'll know we're successful when:** We run marketing for every CDP destination.
### Establish us as an AI-first company (Cleo)
- **Rationale:** This is the direction of travel.
- **Things we could do:** Launch Max AI. Launch Tasks. Land the messaging.
- **Planning issue:** Coming soon?
- **We'll know we're successful when:** Max AI is seen as equal to product analytics.
### Boost cross-sell, especially with startups (Joe)

View File

@@ -19,7 +19,7 @@ template: team
- These are the engineers building the product. Normally full-stack engineers skewing frontend or frontend engineers.
- Product engineers have more limited time. Need to quickly get high-quality insights to inform what they are building and assess what they've shipped.
- Not a focus but should be usable by:
- Not a focus, but should be usable by:
- Other engineers
- Marketing
- Everyone in the product team (PMs, designers)
@@ -27,7 +27,7 @@ template: team
### What types of companies?
We want to build something that will convince our existing large (20k+) customers to turn PostHog on on their marketing website.
- We are building web analytics for high-growth startups that have PMF. Our product should be useful for them right from the start.
### Jobs to be done
@@ -47,10 +47,19 @@ Additionally, it should be:
## Roadmap
### 3 year goals
### 5-year vision
- Be useful for the best high growth companies right from the start
- Lead companies into other PostHog products as they start to need them, massively boosting them
- Run on cheap and fast infrastructure
- Can scale to the biggest websites in the world
- Solve the extremely common problem of marketing and product using completely siloed data stacks
**Web analytics as the command center**
Web analytics becomes the central command hub for everything that happens on the web. It unites acquisition, performance, and behavior data in one intelligent view. It is the 80/20 dashboard for all related PostHog products, connecting directly to experiments, sessions, and insights. No longer a passive reporting tool, its where teams take action.
**From reactive to proactive**
Analysis turns into action. Web analytics doesnt just highlight regressions; it solves them. When conversion rates drop or core web vitals slip, it can open a GitHub PR, create a task, or launch a follow-up experiment automatically.
**A self-optimizing web**
Everything related to the web lives here: marketing spend, attribution, churn risk, SEO/AEO/GEO, and performance metrics, all in one continuous feedback loop. PostHog AI connects the dots across marketing, performance, and behavior, finding opportunities for improvement, generating new assets or variants, and running experiments autonomously.
**From chart-drilling to conversations**
Instead of drilling into dashboards, users simply chat with web analytics. They ask questions, get clear explanations, and receive ready-to-apply actions, all in natural language. The focus shifts from just analyzing data to acting on insights.
**Analytics for the agent era**
As AI agents and LLMs become real web users, PostHog helps teams measure, understand, and optimize for them. Web analytics will distinguish between agent traffic and human traffic, paving the way for the area and empowering teams to tailor their experiences for both human and AI audiences.

View File

@@ -122,7 +122,7 @@
"parse-link-header": "^2.0.0",
"patch-package": "^8.0.0",
"pluralize": "^8.0.0",
"posthog-js": "1.288.1",
"posthog-js": "1.290.0",
"posthog-node": "^4.2.0",
"prism-react-renderer": "^1.3.5",
"prismjs": "^1.29.0",

View File

@@ -3,7 +3,7 @@ import * as am5 from '@amcharts/amcharts5'
import * as am5map from '@amcharts/amcharts5/map'
import am5geodata_worldLow from '@amcharts/amcharts5-geodata/worldLow'
import am5themes_Animated from '@amcharts/amcharts5/themes/Animated'
type ExclusionReason = 'sanctions' | 'high-cost' | 'contractors' | 'germany' | 'timezone'
type ExclusionReason = 'sanctions' | 'high-cost' | 'contractors' | 'timezone'
interface CountryRestriction {
code: string
@@ -34,9 +34,6 @@ const countryRestrictions: { [key: string]: CountryRestriction } = {
Brazil: { code: 'BR', reason: 'contractors' },
Uruguay: { code: 'UY', reason: 'contractors' },
// Germany (10 employee limit)
Germany: { code: 'DE', reason: 'germany' },
// Outside timezone range (UTC+3 and beyond, or UTC-9 and beyond)
Afghanistan: { code: 'AF', reason: 'timezone' },
Armenia: { code: 'AM', reason: 'timezone' },
@@ -54,6 +51,7 @@ const countryRestrictions: { [key: string]: CountryRestriction } = {
Eritrea: { code: 'ER', reason: 'timezone' },
Ethiopia: { code: 'ET', reason: 'timezone' },
Fiji: { code: 'FJ', reason: 'timezone' },
'French Polynesia': { code: 'PF', reason: 'timezone' },
Georgia: { code: 'GE', reason: 'timezone' },
India: { code: 'IN', reason: 'timezone' },
Indonesia: { code: 'ID', reason: 'timezone' },
@@ -74,6 +72,7 @@ const countryRestrictions: { [key: string]: CountryRestriction } = {
Myanmar: { code: 'MM', reason: 'timezone' },
Nauru: { code: 'NR', reason: 'timezone' },
Nepal: { code: 'NP', reason: 'timezone' },
'New Caledonia': { code: 'NC', reason: 'timezone' },
'New Zealand': { code: 'NZ', reason: 'timezone' },
Oman: { code: 'OM', reason: 'timezone' },
Pakistan: { code: 'PK', reason: 'timezone' },
@@ -88,6 +87,7 @@ const countryRestrictions: { [key: string]: CountryRestriction } = {
Somalia: { code: 'SO', reason: 'timezone' },
'South Korea': { code: 'KR', reason: 'timezone' },
'Sri Lanka': { code: 'LK', reason: 'timezone' },
Taiwan: { code: 'TW', reason: 'timezone' },
Tajikistan: { code: 'TJ', reason: 'timezone' },
Tanzania: { code: 'TZ', reason: 'timezone' },
Thailand: { code: 'TH', reason: 'timezone' },
@@ -108,7 +108,6 @@ const reasonColors: { [key in ExclusionReason]: number } = {
sanctions: 0xf35454, // Red
'high-cost': 0xb62ad9, // Purple
contractors: 0xf7a501, // Orange (will have pattern)
germany: 0x3b2b26, // Dark brown
timezone: 0xaaaaaa, // Light gray
}
@@ -116,7 +115,6 @@ const reasonLabels: { [key in ExclusionReason]: string } = {
sanctions: 'Restricted due to US sanctions',
'high-cost': 'High employer costs',
contractors: 'Hired as contractors',
germany: 'No longer hiring (limited to 10 employees)',
timezone: 'Outside of timezones we currently hire in',
}
@@ -290,10 +288,6 @@ export default function CountriesWeHireIn({
<div className="w-4 h-4 bg-purple rounded-sm" />
<span>High employer costs</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-brown rounded-sm" />
<span>Limited to 10 employees</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray rounded-sm" />
<span>Outside hiring timezone range</span>

View File

@@ -1,22 +1,44 @@
import React, { useRef } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { DotLottiePlayer } from '@dotlottie/react-player'
import { useChat } from 'hooks/useChat'
import ScrollArea from 'components/RadixUI/ScrollArea'
import { useApp } from '../../context/App'
export default function InkeepEmbeddedChat(): JSX.Element {
const chatFunctionsRef = useRef(null)
const lottieRef = useRef(null)
const { EmbeddedChat, aiChatSettings, baseSettings } = useChat()
const { isMobile } = useApp()
const [initialQuestionAsked, setInitialQuestionAsked] = useState(false)
const [chatRefReady, setChatRefReady] = useState(false)
const { EmbeddedChat, aiChatSettings, baseSettings, initialQuestion } = useChat()
const { isMobile, windows, closeWindow } = useApp()
const Container = isMobile ? ScrollArea : React.Fragment
const setChatFunctionsRef = useCallback((instance: any) => {
chatFunctionsRef.current = instance
setChatRefReady(true)
}, [])
useEffect(() => {
if (initialQuestion && !initialQuestionAsked && chatRefReady) {
chatFunctionsRef.current?.submitMessage?.(initialQuestion)
setInitialQuestionAsked(true)
const searchWindow = windows.find((w) => w.key === 'search')
if (searchWindow) {
closeWindow(searchWindow)
}
}
}, [chatRefReady])
return (
<>
{EmbeddedChat ? (
<div id="embedded-chat-target" className="h-full">
<Container>
<EmbeddedChat aiChatSettings={aiChatSettings} baseSettings={baseSettings} />
<EmbeddedChat
aiChatSettings={{ ...aiChatSettings, chatFunctionsRef: setChatFunctionsRef }}
baseSettings={baseSettings}
/>
</Container>
</div>
) : (

View File

@@ -3,6 +3,7 @@ export const sfBenchmark: Record<string, number> = {
'Account Executive (OTE)': 300000,
'Backend Engineer': 243000,
'Billing Support Specialist': 154619,
'Business Development Representative (OTE)': 182000,
'Content Marketer': 190910,
'Community Manager': 185000,
'Customer Success Manager (OTE)': 211000,

View File

@@ -1,164 +1,63 @@
import CloudinaryImage from 'components/CloudinaryImage'
import { Check2 } from 'components/Icons'
import Layout from 'components/Layout'
import Link from 'components/Link'
import React from 'react'
import SEO from 'components/seo'
import { StaticImage } from 'gatsby-plugin-image'
import React, { useState, useRef, useEffect } from 'react'
import { useValues } from 'kea'
import { layoutLogic } from 'logic/layoutLogic'
import KeyboardShortcut from 'components/KeyboardShortcut'
import SalesforceForm from 'components/SalesforceForm'
import TeamMember from 'components/TeamMember'
import { Script } from 'gatsby'
import Editor from 'components/Editor'
import ScrollArea from 'components/RadixUI/ScrollArea'
import SalesforceForm from 'components/SalesforceForm'
const features = [
'Volume discounts',
'SAML SSO',
'Custom MSA',
'Dedicated support',
'Personalized onboarding',
'Advanced permissions & audit logs',
]
const VideoSection = () => (
<section
id="demo-video"
className={`overflow-hidden transition-all duration-300 h-auto max-h-[90vh] border border-primary rounded leading-[0] shadow-xl mb-8`}
>
<iframe
src="https://www.youtube-nocookie.com/embed/2jQco8hEvTI?autoplay=1"
className="rounded w-full aspect-video m-0"
allow="autoplay"
/>
</section>
)
export default function ContactSales({ location }) {
const [showVideo, setShowVideo] = useState(false)
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768)
interface ContactSalesProps {
formConfig?: {
type: 'lead' | 'contact'
formOptions?: {
className?: string
cols?: 1 | 2
ctaLocation?: 'top' | 'bottom'
showToField?: boolean | undefined
rowPadding?: string
}
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
const handleShowVideo = () => {
setShowVideo(true)
if (!isMobile) {
setTimeout(() => {
window.scrollTo({
top: document.getElementById('demo-video').offsetTop - 80,
behavior: 'smooth',
})
}, 100)
form: {
fields: {
label: string
placeholder?: string
type: 'string' | 'enumeration'
name: string
required?: boolean
options?: { label: string; value: string | number }[]
fieldType?: string
cols?: 1 | 2
}[]
ctaButton: {
label?: string
width?: 'full' | 'auto'
icon?: React.ReactNode | null
size?: 'sm' | 'md' | 'lg' | 'absurd'
type?: 'primary' | 'secondary' | 'outline'
}
message?: string
name: string
}
customMessage?: React.ReactNode
onSubmit?: (values: any) => void
customFields?: {
[key: string]: {
type: 'radioGroup'
options?: { label: string; value: string | number }[]
cols?: 1 | 2
}
}
autoValidate?: boolean
source?: string
}
}
export default function ContactSales({ formConfig }: ContactSalesProps) {
if (!formConfig) {
return null
}
return (
<>
<Script id="default-form-script" src="/scripts/default-form-script.js" />
<SEO
title="Talk to a human Book a PostHog demo"
description="PostHog is self-serve, but our team is here if you need us. Book a demo to get setup help, discuss your technical requirements, or see features in action."
image={`/images/og/talk-to-a-human.png`}
/>
<ScrollArea>
<div data-scheme="primary" className="bg-accent text-primary h-full" data-default-form-id="509041">
<SalesforceForm
type="lead"
buttonOptions={{
size: 'md',
}}
formOptions={{
className: 'pb-4 flex flex-col',
}}
form={{
fields: [
{
label: 'From',
placeholder: 'Your email',
type: 'string',
name: 'email',
required: true,
fieldType: 'email',
},
{
label: 'Company',
type: 'string',
name: 'company',
required: true,
},
{
label: 'Role',
name: 'role',
type: 'enumeration',
options: [
{
label: 'Engineering',
value: 'Engineering',
},
{
label: 'Founder',
value: 'Founder',
},
{
label: 'Leadership',
value: 'Leadership',
},
{
label: 'Marketing',
value: 'Marketing',
},
{
label: 'Product',
value: 'Product',
},
{
label: 'Sales',
value: 'Sales',
},
{
label: 'Other',
value: 'Other',
},
],
required: true,
},
{
label: 'Monthly active users',
name: 'monthly_active_users',
type: 'string',
fieldType: 'number',
required: true,
},
{
label: 'What do you want to talk about on the call?',
name: 'talk_about',
type: 'string',
required: true,
fieldType: 'textarea',
},
{
label: 'Where did you hear about us?',
type: 'string',
name: 'where_did_you_hear_about_us',
required: false,
},
],
buttonText: 'Send',
message: "Message received! We'll be in touch.",
name: 'Contact sales',
}}
/>
</div>
</ScrollArea>
<SalesforceForm {...formConfig} />
</>
)
}

View File

@@ -183,18 +183,25 @@ export default function HeaderBar({
{homeURL && <OSButton size="md" icon={<IconHome />} to={homeURL} asLink />}
<div>
{hasLeftSidebar && (
<OSButton
size="md"
onClick={onToggleNav}
active={isNavVisible}
icon={
isNavVisible ? (
<IconSidebarOpen className={navIconClassName} />
) : (
<IconSidebarClose className={navIconClassName} />
)
<Tooltip
trigger={
<OSButton
size="md"
onClick={onToggleNav}
active={isNavVisible}
icon={
isNavVisible ? (
<IconSidebarOpen className={navIconClassName} />
) : (
<IconSidebarClose className={navIconClassName} />
)
}
/>
}
/>
>
{isNavVisible ? 'Hide' : 'Show'}
{slideId ? ` slides` : ''}
</Tooltip>
)}
</div>
</motion.div>

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react'
import ParseHtml from '../Utilities/parseHtml'
import { DemoScheduler } from 'components/DemoScheduler'
import SalesRep from '../Utilities/SalesRep'
import TeamMembers from '../Utilities/TeamMembers'
import Logos from '../Utilities/Logos'
import OSButton from 'components/OSButton'
@@ -16,7 +17,8 @@ interface ColumnsTemplateProps {
textColor?: string
companyLogo?: string
companyName?: string
salesRep?: SalesRep
salesRep?: SalesRep | null
teamSlug?: string
slideKey?: string
}
@@ -32,6 +34,7 @@ export default function ColumnsTemplate({
companyLogo,
companyName,
salesRep,
teamSlug,
slideKey,
}: ColumnsTemplateProps) {
return (
@@ -64,7 +67,7 @@ export default function ColumnsTemplate({
</div>
</div>
<SalesRep salesRep={salesRep} />
<TeamMembers salesRep={salesRep} teamSlug={teamSlug} />
</div>
<aside className="flex-1 flex items-center justify-center">

View File

@@ -4,6 +4,14 @@ import CloudinaryImage from 'components/CloudinaryImage'
import useProduct from 'hooks/useProduct'
import ScrollArea from 'components/RadixUI/ScrollArea'
interface SalesRep {
name: string
title: string
email: string
photo: string
color: string
}
interface ContentItem {
handle: string
title: string
@@ -22,7 +30,7 @@ interface ColumnsTemplateProps {
textColor?: string
companyLogo?: string
companyName?: string
salesRep?: SalesRep
salesRep?: SalesRep | null
slideKey?: string
content?: ContentItem[]
}

View File

@@ -30,7 +30,7 @@ interface PricingTemplateProps {
textColor?: string
companyLogo?: string
companyName?: string
salesRep?: SalesRep
salesRep?: SalesRep | null
slideKey?: string
}

View File

@@ -31,7 +31,7 @@ interface StackedTemplateProps {
textColor?: string
companyLogo?: string
companyName?: string
salesRep?: SalesRep
salesRep?: SalesRep | null
slideKey?: string
}
@@ -109,7 +109,7 @@ export default function StackedTemplate({
/>
{description && (
<ParseHtml
content={description.replace('{companyName}', companyName || '')}
content={description.replace('{companyName}', companyName || 'your product')}
className={`prose text-2xl @2xl:text-xl @2xl:text-balance ${
image ? '' : descriptionWidth
}`}

View File

@@ -0,0 +1,168 @@
import { IconSend } from '@posthog/icons'
import ContactSales from 'components/ContactSales'
import React from 'react'
import ScrollArea from 'components/RadixUI/ScrollArea'
import TeamMembers from './TeamMembers'
import { Accordion } from 'components/RadixUI/Accordion'
interface SalesRep {
name: string
title: string
email: string
photo: string
color: string
}
interface PresentationFormProps {
teamSlug?: string
salesRep?: SalesRep | null
}
const formConfig = {
type: 'lead' as const,
formOptions: {
className: 'flex flex-col',
ctaLocation: 'bottom' as const,
showToField: false,
rowPadding: '',
},
form: {
fields: [
{
label: 'From',
placeholder: 'Your email',
type: 'string' as const,
name: 'email',
required: true,
fieldType: 'email',
},
{
label: 'Company',
type: 'string' as const,
name: 'company',
required: true,
},
{
label: 'Role',
name: 'role',
type: 'enumeration' as const,
options: [
{
label: 'Engineering',
value: 'Engineering',
},
{
label: 'Founder',
value: 'Founder',
},
{
label: 'Leadership',
value: 'Leadership',
},
{
label: 'Marketing',
value: 'Marketing',
},
{
label: 'Product',
value: 'Product',
},
{
label: 'Sales',
value: 'Sales',
},
{
label: 'Other',
value: 'Other',
},
],
required: true,
},
{
label: 'Monthly active users',
name: 'monthly_active_users',
type: 'string' as const,
fieldType: 'number',
required: true,
},
{
label: 'What do you want to chat about?',
name: 'talk_about',
type: 'string' as const,
required: true,
fieldType: 'textarea',
},
{
label: 'Where did you hear about us?',
type: 'string' as const,
name: 'where_did_you_hear_about_us',
required: false,
},
],
ctaButton: {
label: 'Send message',
size: 'md',
type: 'primary',
width: 'full',
},
message: "Message received! We'll be in touch.",
name: 'Contact sales',
},
}
export default function PresentationForm({ teamSlug, salesRep }: PresentationFormProps) {
return (
<ScrollArea viewportClasses="[&>div]:h-full">
<div data-scheme="primary" className="bg-accent text-primary h-full p-4" data-default-form-id="509041">
<div className="prose prose-sm dark:prose-invert mb-4">
<h3 className="text-xl mb-1">Get a demo</h3>
<p className="text-sm">
Fear not, our "sales calls" are more like nerdy chats with an old friend. We're here to be
helpful, not to force our products on you.
</p>
<Accordion
items={[
{
trigger: '30-min call agenda',
content: (
<>
<ol className="my-0">
<li>
Intro <span className="text-muted">(10 min)</span>
<ul className="pl-2 mt-0 text-[13px]">
<li>Friendly banter (but not too much)</li>
<li>Understand your needs</li>
<li>Explain our vision</li>
</ul>
</li>
<li>
Demo <span className="text-muted">(15 min)</span>
<ul className="pl-2 mt-0 text-[13px]">
<li>Tailored product demo</li>
<li>Docs and resources</li>
</ul>
</li>
<li>
Wrap-up <span className="text-muted">(5 min)</span>
<ul className="pl-2 mt-0 text-[13px]">
<li>Answer questions</li>
<li>
<s>Pressure tactics</s>
</li>
</ul>
</li>
</ol>
</>
),
},
]}
/>
</div>
<TeamMembers teamSlug={teamSlug} salesRep={salesRep} />
<ContactSales formConfig={formConfig as any} />
</div>
</ScrollArea>
)
}

View File

@@ -0,0 +1,160 @@
import CloudinaryImage from 'components/CloudinaryImage'
import Tooltip from 'components/RadixUI/Tooltip'
import { graphql, useStaticQuery } from 'gatsby'
import React from 'react'
interface SalesRep {
name: string
title: string
email: string
photo: string
color: string
}
interface TeamMembersProps {
teamSlug?: string
salesRep?: SalesRep | null
}
export default function TeamMembers({ teamSlug = 'sales-product-led', salesRep }: TeamMembersProps) {
const { allTeams } = useStaticQuery(graphql`
{
allTeams: allSqueakTeam {
nodes {
id
name
slug
profiles {
data {
id
attributes {
color
firstName
lastName
avatar {
data {
attributes {
url
}
}
}
}
}
}
leadProfiles {
data {
id
}
}
}
}
}
`)
// If a specific sales rep is assigned, show only that person
if (salesRep && salesRep.name && salesRep.photo) {
return (
<div className="border border-primary p-4 rounded bg-primary mb-4">
<h3 className="text-sm mb-0.5">Your helpful PostHog person</h3>
<p className="text-xs mb-3">This friendly face would love to chat with you.</p>
<div className="flex items-center gap-3">
<CloudinaryImage
src={salesRep.photo as `https://res.cloudinary.com/${string}`}
alt={salesRep.name}
className={`size-16 rounded-full overflow-hidden border-2 border-${salesRep.color} p-[1.5px]`}
imgClassName={`object-cover rounded-full bg-${salesRep.color}`}
width={80}
/>
<div className="text-left">
<div className="text-base font-semibold @2xl:leading-tight">{salesRep.name}</div>
<div className="text-[13px] text-secondary @2xl:leading-tight">{salesRep.title}</div>
<a
href={`mailto:${salesRep.email}`}
className="block pt-0.5 text-sm underline font-semibold @2xl:leading-tight"
>
{salesRep.email}
</a>
</div>
</div>
</div>
)
}
// Find the team by slug
const team = allTeams.nodes.find((t: any) => t.slug === teamSlug)
if (!team || !team.profiles?.data?.length) {
return null
}
const profiles = team.profiles.data
const leadProfiles = team.leadProfiles?.data || []
// Sort profiles to show team leads first
const sortedProfiles = profiles.slice().sort((a: any, b: any) => {
const aIsLead = leadProfiles.some(({ id: leadID }: { id: string }) => leadID === a.id)
const bIsLead = leadProfiles.some(({ id: leadID }: { id: string }) => leadID === b.id)
return aIsLead === bIsLead ? 0 : aIsLead ? -1 : 1
})
return (
<div>
<div
data-scheme="primary"
className="relative justify-center transform transition-all duration-100 border border-primary bg-primary rounded p-4 mb-4"
>
<h3 className="text-sm mb-1">Our {team.name} Team</h3>
<p className="text-sm mb-3">One of these friendly faces would love to chat with you.</p>
<div className="flex flex-wrap justify-end ml-3" dir="rtl">
{profiles.length > 8 && (
<span className="visible cursor-default -ml-3 relative hover:z-10 rounded-full border-1 border-primary">
<Tooltip
trigger={
<div className="size-10 rounded-full bg-accent border border-light dark:border-dark flex items-center justify-center text-sm font-semibold transform scale-100 hover:scale-125 transition-all">
{profiles.length - 7}+
</div>
}
side="bottom"
>
{profiles.length - 7} more
</Tooltip>
</span>
)}
{sortedProfiles
.slice(0, profiles.length > 8 ? 7 : undefined)
.reverse()
.map(({ id, attributes: { firstName, lastName, avatar, color } }: any, index: number) => {
const name = [firstName, lastName].filter(Boolean).join(' ')
const isTeamLead = leadProfiles.some(({ id: leadID }: { id: string }) => leadID === id)
return (
<span
key={`${name}-${index}`}
className={`visible cursor-default -ml-3 relative hover:z-10 transform scale-100 hover:scale-125 transition-all rounded-full border-1 ${
isTeamLead ? 'border-yellow dark:border-yellow' : 'border-primary'
}`}
>
<Tooltip
trigger={
<img
src={avatar?.data?.attributes?.url}
className={`size-10 rounded-full bg-${
color ?? 'accent'
} border border-light dark:border-dark`}
alt={name}
/>
}
side="bottom"
delay={0}
>
{name} {isTeamLead ? '(Team lead)' : ''}
</Tooltip>
</span>
)
})}
</div>
</div>
</div>
)
}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useMemo } from 'react'
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import HeaderBar from 'components/OSChrome/HeaderBar'
import ScrollArea from 'components/RadixUI/ScrollArea'
import { Accordion } from '../RadixUI/Accordion'
@@ -9,12 +9,22 @@ import { motion } from 'framer-motion'
import { navigate } from 'gatsby'
import { useWindow } from '../../context/Window'
import { useApp } from '../../context/App'
import ContactSales from 'components/ContactSales'
import PresentationForm from './Utilities/PresentationForm'
interface AccordionItem {
title: string
content: React.ReactNode
}
interface SalesRep {
name: string
title: string
email: string
photo: string
color: string
}
interface PresentationProps {
template: 'generic' | 'product' | 'feature'
slug: string
@@ -32,6 +42,13 @@ interface PresentationProps {
}>
slideId?: string
presenterNotes?: Record<string, string>
config?: {
thumbnails?: boolean
notes?: boolean
form?: boolean
teamSlug?: string
}
salesRep?: SalesRep | null
}
const SidebarContent = ({
@@ -81,6 +98,14 @@ export const getIsMobile = (siteSettings: any, appWindow: any) => {
return width < 672
}
// Extract query param reading logic - DRY principle
const getPanelStateFromURL = (param: string, configDefault?: boolean): boolean => {
if (typeof window === 'undefined') return configDefault ?? true
const params = new URLSearchParams(window.location.search)
const value = params.get(param)
return value !== null ? value === 'true' : configDefault ?? true
}
export default function Presentation({
accentImage,
sidebarContent,
@@ -89,51 +114,82 @@ export default function Presentation({
slides = [],
slideId,
presenterNotes,
config,
salesRep,
}: PresentationProps) {
const { siteSettings } = useApp()
const { appWindow } = useWindow()
const [isMobile, setIsMobile] = useState<boolean>(getIsMobile(siteSettings, appWindow))
const [isNavVisible, setIsNavVisible] = useState<boolean>(!isMobile)
// Lazy initializers read state once on mount - prevents flash of wrong state
const [isNavVisible, setIsNavVisible] = useState<boolean>(() =>
getPanelStateFromURL('thumbnails', config?.thumbnails)
)
const [isPresentationMode, setIsPresentationMode] = useState<boolean>(false)
const [currentSlideIndex, setCurrentSlideIndex] = useState<number>(0)
const [activeSlideIndex, setActiveSlideIndex] = useState<number>(0)
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(true)
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(() => getPanelStateFromURL('notes', config?.notes))
const [isFormVisible, setIsFormVisible] = useState<boolean>(() =>
getPanelStateFromURL('form', config?.form ?? false)
)
const [drawerHeight, setDrawerHeight] = useState<number>(90)
const [lastOpenHeight, setLastOpenHeight] = useState<number>(90)
const [isDragging, setIsDragging] = useState<boolean>(false)
const [dragStartHeight, setDragStartHeight] = useState<number>(0)
const containerRef = useRef<HTMLDivElement>(null)
const toggleNav = () => {
setIsNavVisible(!isNavVisible)
}
// Update URL when panel states change - preserves all existing params
const updateURL = useCallback((thumbnails: boolean, notes: boolean) => {
if (typeof window === 'undefined') return
const toggleDrawer = () => {
if (isDrawerOpen) {
// Closing: save current height (only if it's reasonable)
if (drawerHeight >= 10) {
setLastOpenHeight(drawerHeight)
const params = new URLSearchParams(window.location.search)
params.set('thumbnails', String(thumbnails))
params.set('notes', String(notes))
const newURL = `${window.location.pathname}?${params.toString()}${window.location.hash}`
window.history.replaceState({}, '', newURL)
}, [])
const toggleNav = useCallback(() => {
setIsNavVisible((prev) => {
const newState = !prev
updateURL(newState, isDrawerOpen)
return newState
})
}, [isDrawerOpen, updateURL])
const toggleDrawer = useCallback(() => {
setIsDrawerOpen((prev) => {
if (prev) {
// Closing: save current height if reasonable
if (drawerHeight >= 10) {
setLastOpenHeight(drawerHeight)
}
updateURL(isNavVisible, false)
return false
} else {
// Opening: restore last height
const heightToRestore = lastOpenHeight >= 25 ? lastOpenHeight : 90
setDrawerHeight(heightToRestore)
updateURL(isNavVisible, true)
return true
}
setIsDrawerOpen(false)
} else {
// Opening: restore last height, but ensure it's reasonable
const heightToRestore = lastOpenHeight >= 25 ? lastOpenHeight : 90
setDrawerHeight(heightToRestore)
setIsDrawerOpen(true)
}
}
})
}, [drawerHeight, lastOpenHeight, isNavVisible, updateURL])
const handleVerticalDrag = (_event: any, info: any) => {
if (!containerRef.current || !isDrawerOpen) return
const containerHeight = containerRef.current.getBoundingClientRect().height
const newDrawerHeight = Math.min(Math.max(dragStartHeight - info.offset.y, 0), 300)
setDrawerHeight(newDrawerHeight)
const handleVerticalDrag = useCallback(
(_event: any, info: any) => {
if (!containerRef.current || !isDrawerOpen) return
const newDrawerHeight = Math.min(Math.max(dragStartHeight - info.offset.y, 0), 300)
setDrawerHeight(newDrawerHeight)
// Update lastOpenHeight for reasonable heights only
if (newDrawerHeight >= 10) {
setLastOpenHeight(newDrawerHeight)
}
}
// Update lastOpenHeight for reasonable heights only
if (newDrawerHeight >= 10) {
setLastOpenHeight(newDrawerHeight)
}
},
[dragStartHeight, isDrawerOpen]
)
const currentSlideNotes = useMemo(() => {
if (!presenterNotes || !slides || slides.length === 0) return ''
@@ -248,21 +304,35 @@ export default function Presentation({
return () => window.removeEventListener('resize', handleResize)
}, [appWindow, siteSettings])
// Handle mobile/desktop transitions - don't persist mobile behavior to URL
useEffect(() => {
setIsNavVisible(!isMobile)
}, [isMobile])
if (isMobile) {
setIsNavVisible(false)
} else {
setIsNavVisible(getPanelStateFromURL('thumbnails', config?.thumbnails))
}
}, [isMobile, config])
const enterPresentationMode = () => {
// Use the currently active slide index instead of searching for visible slide
// Update drawer state when config changes
useEffect(() => {
setIsDrawerOpen(getPanelStateFromURL('notes', config?.notes))
}, [config])
// Update form visibility when config changes
useEffect(() => {
setIsFormVisible(getPanelStateFromURL('form', config?.form ?? false))
}, [config])
const enterPresentationMode = useCallback(() => {
if (slides.length > 0) {
setCurrentSlideIndex(activeSlideIndex)
}
setIsPresentationMode(true)
}
}, [slides.length, activeSlideIndex])
const exitPresentationMode = () => {
const exitPresentationMode = useCallback(() => {
setIsPresentationMode(false)
}
}, [])
return (
<>
@@ -393,6 +463,15 @@ export default function Presentation({
</>
)}
</main>
{/* editor/form panel */}
{isFormVisible && (
<aside
data-scheme="secondary"
className="w-80 h-full bg-primary border-l border-primary hidden @2xl:block"
>
<PresentationForm teamSlug={config?.teamSlug} salesRep={salesRep} />
</aside>
)}
</div>
</div>

View File

@@ -128,7 +128,10 @@ export const Calculator = ({ SidebarList, SidebarListItem, Discounts }: Calculat
<div className="flex flex-col @6xl:flex-row @6xl:gap-8 items-start">
<Tabbed />
<div className="grid @2xl:grid-cols-2 @6xl:grid-cols-1 gap-8 mt-12 @6xl:mt-0 @6xl:max-w-xs sticky top-4">
<div
id="discounts"
className="grid @2xl:grid-cols-2 @6xl:grid-cols-1 gap-8 mt-12 @6xl:mt-0 @6xl:max-w-xs sticky top-4"
>
<div>
<h4 className="text-lg mb-2">How our pricing works</h4>
<SidebarList>

View File

@@ -38,7 +38,7 @@ const AccordionTrigger = React.forwardRef<HTMLButtonElement, AccordionTriggerPro
<RadixAccordion.Header className="flex">
<RadixAccordion.Trigger
className={`group flex flex-1 items-center justify-between px-2 py-1 text-sm leading-none select-none ${
skin ? 'first:rounded-t bg-accent hover:bg-accent' : ''
skin ? 'first:rounded-t last:rounded-b bg-accent hover:bg-accent' : ''
} text-primary outline-none ${className}`}
{...props}
ref={forwardedRef}

View File

@@ -25,14 +25,12 @@ interface IProps {
cols?: 1 | 2
}
}
buttonOptions?: {
className?: string
size?: 'sm' | 'md' | 'lg' | 'absurd'
type?: 'primary' | 'secondary' | 'outline'
}
formOptions?: {
className?: string
cols?: 1 | 2
ctaLocation?: 'top' | 'bottom'
showToField?: boolean
rowPadding?: string
}
autoValidate?: boolean
form: {
@@ -45,8 +43,15 @@ interface IProps {
options?: CustomFieldOption[]
fieldType?: string
cols?: 1 | 2
hideLabel?: boolean
}[]
buttonText?: string
ctaButton: {
label?: string
width?: 'full' | 'auto'
icon?: React.ReactNode | null
size?: 'sm' | 'md' | 'lg' | 'absurd'
type?: 'primary' | 'secondary' | 'outline'
}
message?: string
name: string
}
@@ -260,8 +265,38 @@ function RadioGroup({
const inputContainerClasses = `relative text-left text-sm col-span-full @lg:col-span-2 font-semibold flex items-center`
const Textarea = (props: InputHTMLAttributes<HTMLTextAreaElement>) => {
const { name, placeholder, required } = props
interface CTAButtonProps {
location?: 'top' | 'bottom'
width?: 'full' | 'auto'
size?: 'sm' | 'md' | 'lg' | 'xs' | 'xl'
variant?: 'primary' | 'secondary' | 'default' | 'underline' | 'underlineOnHover'
icon?: React.ReactNode
label?: string
rowPadding?: string
}
const CTAButton = ({ location, width, size, variant, icon, label, rowPadding }: CTAButtonProps) => {
return (
<div
className={`flex-[0_0_auto] ${location === 'top' ? 'py-2 border-primary border-b mb-4' : 'pt-1'} ${
rowPadding || ''
}`}
>
<OSButton
width={width || 'auto'}
size={size || 'md'}
variant={variant || 'primary'}
icon={icon}
type="submit"
>
{label || 'Submit'}
</OSButton>
</div>
)
}
const Textarea = (props: InputHTMLAttributes<HTMLTextAreaElement> & { className?: string }) => {
const { name, placeholder, required, className } = props
if (!name) return null
const { errors, validateField, setFieldValue } = useFormikContext<Record<string, any>>()
const error = (errors as any)[name]
@@ -274,7 +309,7 @@ const Textarea = (props: InputHTMLAttributes<HTMLTextAreaElement>) => {
{required && <span className="text-red dark:text-yellow ml-0.5">*</span>}
</span>
</label>
<div className="col-span-full @lg:col-span-10">
<div className="col-span-full">
<textarea
rows={8}
onChange={(e) => setFieldValue(name, e.target.value)}
@@ -283,7 +318,7 @@ const Textarea = (props: InputHTMLAttributes<HTMLTextAreaElement>) => {
}}
className={`outline-none text-sm rounded border bg-primary ring-0 focus:ring-0 w-full resize-none ${
error ? 'border-red' : 'border-primary'
}`}
} ${className ?? ''}`}
{...props}
{...(props.type === 'number' ? { min: 0 } : {})}
/>
@@ -330,7 +365,6 @@ export default function SalesforceForm({
customFields,
customMessage,
onSubmit,
buttonOptions,
formOptions,
form,
type = 'lead',
@@ -358,10 +392,20 @@ export default function SalesforceForm({
setConfetti(true)
}
const ctaButtonProps: CTAButtonProps = {
location: formOptions?.ctaLocation || 'bottom',
rowPadding: formOptions?.rowPadding || '',
width: form.ctaButton?.width as CTAButtonProps['width'] | undefined,
size: form.ctaButton?.size as CTAButtonProps['size'] | undefined,
variant: form.ctaButton?.type as CTAButtonProps['variant'] | undefined,
icon: form.ctaButton?.icon || undefined,
label: form.ctaButton?.label,
}
return form.fields.length > 0 ? (
submitted ? (
<>
<div className="bg-accent border border-input px-6 py-16 rounded-md flex justify-center items-center">
<div className="bg-primary text-primary border border-green p-4 rounded flex justify-center items-center">
{customMessage || <div dangerouslySetInnerHTML={{ __html: form?.message || '' }} />}
</div>
</>
@@ -400,19 +444,21 @@ export default function SalesforceForm({
onSubmit={handleSubmit}
>
<Form className={formOptions?.className}>
<div className="px-4 pt-2 pb-1 border-b border-primary flex-[0_0_auto]">
<OSButton size="md" variant="primary" icon={<IconSend />} type="submit">
{form.buttonText ?? 'Submit'}
</OSButton>
</div>
{formOptions?.ctaLocation === 'top' && <CTAButton {...ctaButtonProps} />}
<div className="flex-1">
<ScrollArea className="min-h-0">
<div className="@container p-4">
<div className="grid grid-cols-12 gap-2">
<span className="relative text-left text-sm col-span-full @lg:col-span-2 font-semibold flex items-center">
To
</span>
<div className="col-span-full @lg:col-span-10 text-sm">sales@posthog.com</div>
<div className="@container">
<div className={`grid grid-cols-12 gap-2 ${formOptions?.rowPadding || ''}`}>
{formOptions?.showToField && (
<>
<span className="relative text-left text-sm col-span-full @lg:col-span-2 font-semibold flex items-center">
To
</span>
<div className="col-span-full @lg:col-span-10 text-sm">
sales@posthog.com
</div>
</>
)}
{form.fields.map(
(
{ name, label, placeholder, type, required, options, fieldType, cols },
@@ -458,25 +504,38 @@ export default function SalesforceForm({
)
}
)}
{form.fields.map(({ name, label, fieldType, required, hideLabel }, index) => {
if (fieldType === 'textarea') {
return (
<>
{!hideLabel && (
<label className={`${inputContainerClasses}`} htmlFor={name}>
<span>
{label}
{required && (
<span className="text-red dark:text-yellow ml-0.5">
*
</span>
)}
</span>
</label>
)}
<Textarea
key={`${name}-${index}`}
name={name}
placeholder={label}
required={required}
/>
</>
)
}
return null
})}
</div>
</div>
<div className="px-4">
{form.fields.map(({ name, label, fieldType, required }, index) => {
if (fieldType === 'textarea') {
return (
<Textarea
key={`${name}-${index}`}
name={name}
placeholder={label}
required={required}
/>
)
}
return null
})}
</div>
</ScrollArea>
</div>
{formOptions?.ctaLocation === 'bottom' && <CTAButton {...ctaButtonProps} />}
</Form>
</Formik>
)

View File

@@ -6,7 +6,7 @@ import { InstantSearch, useRefinementList } from 'react-instantsearch-hooks-web'
import { useSearchBox, useHits } from 'react-instantsearch-hooks-web'
import { Combobox } from '@headlessui/react'
import { navigate } from 'gatsby'
import { IconFilter } from '@posthog/icons'
import { IconSparkles } from '@posthog/icons'
import { capitalizeFirstLetter } from '../../utils'
import { Hit } from 'instantsearch.js'
import OSButton from 'components/OSButton'
@@ -28,11 +28,11 @@ const Filters = ({ isRefinedClassName = 'bg-primary' }: { isRefinedClassName?: s
onClick={() => {
refine(item.value)
}}
className={`text-sm border border-primary rounded-md px-1 flex space-x-1 items-center ${
className={`text-sm text-primary border border-primary rounded px-1 flex space-x-1 items-center whitespace-nowrap ${
item.isRefined ? isRefinedClassName : ''
}`}
>
<span className="text-sm">{capitalizeFirstLetter(item.label)}</span>{' '}
<span className="text-sm">{capitalizeFirstLetter(item.label.replace(/-/g, ' '))}</span>{' '}
<span className="text-xs opacity-60 font-semibold">({item.count})</span>
</button>
</li>
@@ -59,12 +59,18 @@ const Search = ({
onEscape?: () => void
}) => {
const [query, setQuery] = useState('')
const { dragControls } = useWindow()
const { openNewChat } = useApp()
const { dragControls, appWindow } = useWindow()
const { refine } = useSearchBox()
const { hits } = useHits()
const [showFilters, setShowFilters] = useState(!!initialFilter)
const { refine: filterRefine } = useRefinementList({ attribute: 'type', sortBy: ['name:asc'] })
const openChat = () => {
if (query) {
openNewChat({ path: `ask-max-${appWindow?.path}`, initialQuestion: query })
}
}
const handleChange = (hit: Hit) => {
if (!hit) return
navigate(`/${hit.slug}`, { state: { newWindow: true } })
@@ -72,6 +78,11 @@ const Search = ({
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && e.shiftKey) {
e.preventDefault()
e.stopPropagation()
openChat()
}
if (e.key === 'Escape') {
if (query === '') {
// If input is empty, close the search
@@ -110,22 +121,24 @@ const Search = ({
value={query}
containerClassName="m-0"
/>
{!hideFilters && (
<div data-scheme="secondary" className="absolute right-1 top-1/2 -translate-y-1/2">
<OSButton
size="md"
className={` ${showFilters ? 'opacity-100' : 'opacity-70'}`}
onClick={(e) => {
e.stopPropagation()
setShowFilters(!showFilters)
}}
icon={<IconFilter />}
hover="background"
/>
</div>
)}
<div data-scheme="primary" className="absolute right-1 top-1/2 -translate-y-1/2">
<OSButton
disabled={!query}
size="md"
onClick={(e) => {
e.stopPropagation()
openChat()
}}
icon={<IconSparkles />}
hover="border"
className="font-semibold underline bg-accent text-primary"
>
Ask AI
</OSButton>
</div>
</div>
{!hideFilters && showFilters && <Filters isRefinedClassName={isRefinedClassName} />}
{!hideFilters && hits.length > 0 && query && <Filters isRefinedClassName={isRefinedClassName} />}
{hits.length > 0 && query && (
<Combobox.Options
@@ -172,7 +185,7 @@ export const WindowSearchUI = ({ initialFilter }: { initialFilter?: string }) =>
const { appWindow } = useWindow()
const ref = useRef<HTMLDivElement>(null)
const onChange = () => {
const close = () => {
if (appWindow) {
closeWindow(appWindow)
}
@@ -187,12 +200,12 @@ export const WindowSearchUI = ({ initialFilter }: { initialFilter?: string }) =>
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node) && appWindow) {
closeWindow(appWindow)
close()
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
}, [closeWindow])
return (
<InstantSearch
@@ -204,12 +217,8 @@ export const WindowSearchUI = ({ initialFilter }: { initialFilter?: string }) =>
<Search
initialFilter={initialFilter}
className="cursor-grab active:cursor-grabbing p-2 rounded bg-white/25 backdrop-blur shadow-2xl max-w-screen-md border border-primary"
onChange={onChange}
onEscape={() => {
if (appWindow) {
closeWindow(appWindow)
}
}}
onChange={close}
onEscape={close}
/>
</div>
</InstantSearch>

View File

@@ -386,6 +386,11 @@ export function useMenuData(): MenuType[] {
label: "You'll hate PostHog if...",
link: '/vibe-check',
},
{
type: 'item',
label: "Don't get discount bamboozled",
link: '/discounts',
},
{
type: 'item',
label: 'Social validation for enterprise',
@@ -792,40 +797,40 @@ export function useMenuData(): MenuType[] {
// On mobile, include main navigation items in the logo menu
const logoMenuItems = isMobile
? [
{
type: 'item' as const,
label: 'home.mdx',
link: '/',
},
{ type: 'separator' as const },
// Main navigation items processed for mobile
...processMobileNavItems(),
{ type: 'separator' as const },
// System items
...baseLogoMenuItems,
]
{
type: 'item' as const,
label: 'home.mdx',
link: '/',
},
{ type: 'separator' as const },
// Main navigation items processed for mobile
...processMobileNavItems(),
{ type: 'separator' as const },
// System items
...baseLogoMenuItems,
]
: [
// Desktop: only show system items
...baseLogoMenuItems,
{ type: 'separator' as const },
{
type: 'item' as const,
label: 'Start screensaver',
onClick: () => {
setScreensaverPreviewActive(true)
},
shortcut: ['Shift', 'Z'],
},
{
type: 'item' as const,
label: 'Close all windows',
disabled: windows.length < 1,
onClick: () => {
animateClosingAllWindows()
},
shortcut: ['Shift', 'X'],
},
]
// Desktop: only show system items
...baseLogoMenuItems,
{ type: 'separator' as const },
{
type: 'item' as const,
label: 'Start screensaver',
onClick: () => {
setScreensaverPreviewActive(true)
},
shortcut: ['Shift', 'Z'],
},
{
type: 'item' as const,
label: 'Close all windows',
disabled: windows.length < 1,
onClick: () => {
animateClosingAllWindows()
},
shortcut: ['Shift', 'X'],
},
]
return [
{

View File

@@ -429,7 +429,7 @@ export default function TeamPatch({
textAnchor="middle"
x="138"
>
<tspan y="258.218">{name}</tspan>
<tspan y="262">{name}</tspan>
</TeamName>
</g>
</g>

View File

@@ -96,12 +96,14 @@ interface AppContextType {
quickQuestions,
chatId,
date,
initialQuestion,
}: {
path: string
context?: ChatContext[]
quickQuestions?: string[]
chatId?: string
date?: string
initialQuestion?: string
}) => void
isNotificationsPanelOpen: boolean
setIsNotificationsPanelOpen: (isOpen: boolean) => void
@@ -1066,9 +1068,16 @@ export const Provider = ({ children, element, location }: AppProviderProps) => {
zIndex: win.zIndex,
}))
return savedWindows.length > 0
? `${location.pathname}?${qs.stringify({ windows: savedWindows }, { encode: false })}`
: undefined
if (savedWindows.length === 0) return undefined
// Preserve existing query parameters from the current URL
const currentParams = isSSR ? {} : qs.parse(location.search.substring(1))
const allParams = {
...currentParams,
windows: savedWindows,
}
return `${location.pathname}?${qs.stringify(allParams, { encode: false })}`
}, [windows, taskbarHeight, location, isSSR])
const shareableDesktopURL = useMemo(() => {
@@ -1508,12 +1517,14 @@ export const Provider = ({ children, element, location }: AppProviderProps) => {
quickQuestions,
chatId,
date,
initialQuestion,
}: {
path: string
context?: ChatContext[]
quickQuestions?: string[]
chatId?: string
date?: string
initialQuestion?: string
}) => {
addWindow(
<ChatProvider
@@ -1526,6 +1537,7 @@ export const Provider = ({ children, element, location }: AppProviderProps) => {
quickQuestions={quickQuestions}
chatId={chatId}
date={date}
initialQuestion={initialQuestion}
/>
)
}
@@ -1945,7 +1957,13 @@ export const Provider = ({ children, element, location }: AppProviderProps) => {
if (paramsWindows) {
const [initialWindow, ...rest] = convertWindowsToPixels(parsed.windows)
navigate(initialWindow.path, {
// Preserve non-windows query parameters when navigating
const nonWindowsParams = { ...parsed }
delete nonWindowsParams.windows
const queryString =
Object.keys(nonWindowsParams).length > 0 ? `?${qs.stringify(nonWindowsParams, { encode: false })}` : ''
navigate(`${initialWindow.path}${queryString}`, {
state: {
newWindow: true,
size: initialWindow.size,
@@ -1958,7 +1976,14 @@ export const Provider = ({ children, element, location }: AppProviderProps) => {
if (stateWindows) {
const [nextWindow, ...rest] = stateWindows
if (!nextWindow) return
navigate(nextWindow.path, {
// Preserve query parameters from current URL when navigating to next window
const currentParams = qs.parse(location.search.substring(1))
delete currentParams.windows
const queryString =
Object.keys(currentParams).length > 0 ? `?${qs.stringify(currentParams, { encode: false })}` : ''
navigate(`${nextWindow.path}${queryString}`, {
state: {
newWindow: true,
size: nextWindow.size,

View File

@@ -37,7 +37,7 @@ export const posthog = {
exception_capture: true,
issue_management: true,
log_management: 'Beta',
mobile_sdk_coverage: 'Partial',
mobile_sdk_coverage: true,
profiling: false,
source_map_support: true,
stack_tracing: false,
@@ -47,7 +47,7 @@ export const posthog = {
features: {
cron_monitoring: false,
distributed_tracing: false,
release_tracking: 'Partial',
release_tracking: true,
performance_monitoring: true,
},
},

View File

@@ -33,7 +33,7 @@ export const sentry = {
},
},
integrations: {
session_replay: 'Limited',
session_replay: true,
product_analytics: false,
},
},

View File

@@ -19,6 +19,7 @@ interface ChatContextType {
setContext: (context: { type: 'page'; value: { path: string; label: string } }[]) => void
addContext: (newContext: { type: 'page'; value: { path: string; label: string } }) => void
firstResponse: string | null
initialQuestion?: string
}
const ChatContext = createContext<ChatContextType | undefined>(undefined)
@@ -28,11 +29,13 @@ export function ChatProvider({
quickQuestions: initialQuickQuestions,
chatId,
date,
initialQuestion,
}: {
context?: { type: 'page'; value: { path: string; label: string } }[]
quickQuestions?: string[]
chatId?: string
date?: string
initialQuestion?: string
}): JSX.Element {
const { windows, setWindowTitle } = useApp()
const { appWindow } = useWindow()
@@ -192,6 +195,7 @@ export function ChatProvider({
setContext,
addContext,
firstResponse,
initialQuestion,
}}
>
<Chat />

View File

@@ -437,6 +437,15 @@ const FEATURE_DATA: Record<string, BaseFeature> = {
owner: ['workflows'],
label: 'feature/workflows',
},
wizard: {
feature: 'Wizard',
owner: [],
notes: (
<>
Owned by <TeamMember name="Danilo Campos" photo />, with help from Team Array
</>
),
},
}
export const useFeatureOwnership = ({ teamSlug }: { teamSlug?: string } = {}): { features: Feature[] } => {

View File

@@ -989,6 +989,10 @@ export const handbookSidebar = [
name: 'Events',
url: '/handbook/marketing/events',
},
{
name: 'Customer case studies',
url: '/handbook/marketing/customer-case-studies',
},
{
name: 'Email & comms',
url: '',
@@ -1274,13 +1278,17 @@ export const handbookSidebar = [
url: '/handbook/growth/sales/account-planning',
},
{
name: 'Cross-sell motions',
name: 'Cross selling tactics',
url: '/handbook/growth/sales/cross-selling',
},
{
name: 'Cross sell motions',
url: '/handbook/growth/sales/cross-sell-motions',
},
{
name: 'Communications templates',
url: '/handbook/growth/sales/communications-templates',
},
],
},
{

View File

@@ -1458,6 +1458,17 @@ const CompanyForm = ({ onSuccess, companyId }: { onSuccess?: () => void; company
)}
</OSButton>
</div>
<div>
<p className="text-sm opacity-70 m-0">
Approvals are at our discretion and can take up to 48 hours to process.{' '}
<Link
to="http://app.posthog.com/home#supportModal"
className="text-red dark:text-yellow font-semibold"
>
Got a question?
</Link>
</p>
</div>
</form>
)
}

View File

@@ -0,0 +1,208 @@
import React, { useState, useEffect } from 'react'
import SEO from 'components/seo'
import Editor from 'components/Editor'
import OSButton from 'components/OSButton'
import OSTable from 'components/OSTable'
import { IconRefresh } from '@posthog/icons'
import Link from 'components/Link'
const discountsData = {
actualPricing: {
title: "PostHog's REAL annual pricing",
rows: [
{ item: '20M events w/ profiles', price: '$23,130' },
{ item: '100,000 session recordings', price: '$3,270' },
{ item: '30M data warehouse rows', price: '$4,020' },
{ item: '20% discount', price: '-$6,084' },
{ item: 'Total', price: '$24,336', bold: true },
],
},
extremeDiscounts: {
title: 'PostHog, but with EXTREME DISCOUNTS',
baseRows: [
{ item: '20M events w/ profiles', price: '$36,260' },
{ item: '100,000 session recordings', price: '$4,905' },
{ item: '30M data warehouse rows', price: '$10,050' },
{ item: 'Premium support', price: '$10,000' },
],
total: { item: 'Total (after EXTREME discounts)', price: '$24,336', bold: true },
},
discountNames: [
'I just talked to my manager and... discount',
'End of quarter desperation discount',
'Exclusive HOGS discount',
'Paperless billing discount',
'Quick call discount',
'You know the founders discount',
'Promise to pay on time discount',
'Comped training',
'One time setup fees waived this month only',
'Twitter follower count discount',
'What your last vendor will pay us to take you off their hands discount',
'Your competitor just signed up discount',
'Synergy optimization discount',
'Preempting your RFP discount',
'Thought leader discount',
'We forgot we had this one',
'Not having to call you five times this week',
'Traitor discount',
'AI-powered savings',
'Sign up 5 friends discount',
'Hedgehog hug discount',
],
discountValues: [6084, 12243, 3060, 9182, 6121, 5000, 1274],
}
const PlaceholderComparisonTable = () => {
// Get current day of the week as a string, e.g. 'Monday'
const dayOfWeek = new Date().toLocaleDateString('en-US', { weekday: 'long' })
// Add day-specific discount to the list
const extremeDiscountNames = [...discountsData.discountNames, `${dayOfWeek} discount`]
const discountValues = discountsData.discountValues
// Shuffle and pick a random set of discount names for each render
const getRandomDiscounts = () => {
const shuffled = [...extremeDiscountNames].sort(() => 0.5 - Math.random())
return shuffled.slice(0, discountValues.length)
}
// Use state to allow re-randomizing on button click
const [randomDiscounts, setRandomDiscounts] = useState(getRandomDiscounts())
const handleGenerateDiscounts = () => setRandomDiscounts(getRandomDiscounts())
return (
<div className="mt-4">
<div className="hidden @2xl:flex justify-end">
<OSButton onClick={handleGenerateDiscounts} variant="secondary" size="md">
<IconRefresh className="size-4 inline-block relative -top-px" />
Generate discounts
</OSButton>
</div>
<div className="grid grid-cols-1 @2xl:grid-cols-2 gap-8">
<div className="">
<h4 className="text-xl font-bold mb-0">{discountsData.actualPricing.title}</h4>
<p>This is our honest pricing.</p>
<OSTable
columns={[
{ name: 'Item', align: 'left', width: 'auto' },
{ name: 'Price', align: 'right', width: 'auto' },
]}
rows={discountsData.actualPricing.rows.map((row, i) => ({
cells: [
{ content: row.bold ? <span className="font-bold">{row.item}</span> : row.item },
{
content: row.bold ? <span className="font-bold">{row.price}</span> : row.price,
className: row.bold ? 'border-t border-primary' : undefined,
},
],
}))}
className="!min-w-0 !w-full"
/>
</div>
<div className="">
<h4 className="text-xl font-bold mb-0">{discountsData.extremeDiscounts.title}</h4>
<p>It comes out to the same price but the discounts just make you feel better.</p>
<div className="mb-4 @2xl:hidden">
<OSButton onClick={handleGenerateDiscounts} variant="secondary" size="md">
<IconRefresh className="size-4 inline-block relative -top-px" />
Generate discounts
</OSButton>
</div>
<OSTable
columns={[
{ name: 'Item', align: 'left', width: 'auto' },
{ name: 'Price', align: 'right', width: 'auto' },
]}
rows={[
...discountsData.extremeDiscounts.baseRows.map((row) => ({
cells: [{ content: row.item }, { content: row.price }],
})),
{
cells: [
{
content: <span className="font-bold">EXTREME DISCOUNTS</span>,
className: 'col-span-2',
},
],
},
...(randomDiscounts
.map((name, i) =>
discountValues[i + 1] !== undefined
? {
cells: [
{ content: name },
{ content: `-$${discountValues[i + 1].toLocaleString()}` },
],
}
: null
)
.filter(Boolean) as any[]),
{
cells: [
{
content: (
<span className="font-bold">
{discountsData.extremeDiscounts.total.item}
</span>
),
className: 'border-t border-primary',
},
{
content: (
<span className="font-bold">
{discountsData.extremeDiscounts.total.price}
</span>
),
className: 'border-t border-primary',
},
],
},
]}
className="!min-w-0 !w-full"
/>
</div>
</div>
<p className="mt-8">
If you prefer artificially inflated prices, you might actually enjoy our{' '}
<Link to="/enterprise" state={{ newWindow: true }}>
enterprise
</Link>{' '}
page.
</p>
<p>
Still not sure if PostHog is right for you?{' '}
<Link to="/vibe-check" state={{ newWindow: true }}>
Find out if you'll hate PostHog.
</Link>
</p>
</div>
)
}
export default function Discounts(): JSX.Element {
return (
<>
<SEO
title="Discounts - PostHog"
description="PostHog is the only developer platform built to natively work with Session Replay, Feature Flags, Experiments, and Surveys."
image={`/images/og/default.png`}
/>
<Editor title="Don't get discount bamboozled.">
<p>Some of our competitors use steep discounting to win deals.</p>
<p>
We price our products fairly and offer discounts based on usage and deal terms you can read all
about them{' '}
<Link to="/pricing#discounts" state={{ newWindow: true }}>
on our pricing page.
</Link>
</p>
<p>But sometimes this leaves our customers feeling left out. They want huge discounts too!</p>
<p>
So we decided to offer an EXTREME discount comparison page so you can get all the joy of heavy
discounting without any of the sleazy second-year price increases and vendor lock-in.
</p>
<PlaceholderComparisonTable />
</Editor>
</>
)
}

File diff suppressed because one or more lines are too long

View File

@@ -62,7 +62,9 @@ const errorTrackingFeatures = [
{ text: 'Issue management', url: '/docs/error-tracking/managing-issues' },
{ text: 'Team assignments', url: '/docs/error-tracking/assigning-issues' },
{ text: 'Alerts', url: '/docs/error-tracking/alerts' },
{ text: 'Integrations with Linear and GitHub', url: '/docs/error-tracking/external-tracking' },
{ text: 'Integrations with Linear, GitHub, and GitLab', url: '/docs/error-tracking/external-tracking' },
{ text: 'Track releases and deploys', url: '/docs/error-tracking/releases' },
{ text: 'Mobile support', url: '/docs/error-tracking/installation' },
{ text: 'MCP integration', url: '/docs/error-tracking/debugging-with-mcp' },
{ text: 'Fix with AI', url: '/docs/error-tracking/fix-with-ai-prompts' },
]
@@ -125,6 +127,7 @@ export const Content = () => {
return rows
}, [] as any[])}
size="sm"
width="full"
/>
</section>

View File

@@ -78,11 +78,18 @@ interface SlideConfig {
bgColor?: string
textColor?: string
content?: ContentItem[]
teamSlug?: string
}
interface PresentationConfig {
name: string
slides: Record<string, SlideConfig>
config?: {
thumbnails?: boolean
notes?: boolean
form?: boolean
teamSlug?: string
}
}
const CustomPresentationPage = () => {
@@ -92,16 +99,22 @@ const CustomPresentationPage = () => {
const [salesRep, setSalesRep] = useState<SalesRep | null>(null)
// Parse the URL path
// URL patterns:
// /for/{persona} -> pathSegments = ['', 'for', 'persona']
// /for/{company}/{role} -> pathSegments = ['', 'for', 'company', 'role']
const pathSegments = appWindow?.path ? appWindow?.path.split('/') : []
const companyDomain = pathSegments[2] || ''
const roleOrId = pathSegments[3] || ''
// Redirect to homepage if no company is specified
// Determine if this is a company-specific or persona-only URL
const hasCompany = pathSegments.length >= 4
const companyDomain = hasCompany ? pathSegments[2] : ''
const roleOrId = hasCompany ? pathSegments[3] : pathSegments[2] || ''
// Redirect to homepage if no role/persona is specified
useEffect(() => {
if (!companyDomain) {
if (!roleOrId) {
navigate('/')
}
}, [companyDomain])
}, [roleOrId])
// Load custom configuration if available
const [customConfig, setCustomConfig] = useState<any>(null)
@@ -129,12 +142,21 @@ const CustomPresentationPage = () => {
// Determine which configuration to use
const config: PresentationConfig = useMemo(() => {
// Check if this is a company-specific landing page (has a company domain in the URL)
const isCompanySpecific = !!companyDomain
const defaultTeamSlug = isCompanySpecific ? 'sales-cs' : 'sales-product-led'
// Check for custom configuration first
if (customConfig) {
// Process custom config with inheritance
const processedConfig: PresentationConfig = {
name: customConfig.name || 'Custom Presentation',
slides: {},
config: {
...customConfig.config,
// Set default teamSlug if not specified in custom config
teamSlug: customConfig.config?.teamSlug || defaultTeamSlug,
},
}
Object.entries(customConfig.slides || {}).forEach(([slideKey, slideConfig]: [string, any]) => {
@@ -161,26 +183,48 @@ const CustomPresentationPage = () => {
// Check for standard role configuration
if (roleOrId && roleConfigs[roleOrId as keyof typeof roleConfigs]) {
return roleConfigs[roleOrId as keyof typeof roleConfigs]
const roleConfig = roleConfigs[roleOrId as keyof typeof roleConfigs]
return {
...roleConfig,
config: {
...(roleConfig.config || {}),
// Set default teamSlug if not specified in role config
teamSlug: roleConfig.config?.teamSlug || defaultTeamSlug,
},
}
}
// Default configuration
return defaultConfig
}, [roleOrId, customConfig])
return {
...defaultConfig,
config: {
...(defaultConfig.config || {}),
// Set default teamSlug if not specified in default config
teamSlug: defaultConfig.config?.teamSlug || defaultTeamSlug,
},
}
}, [roleOrId, customConfig, companyDomain])
// Fetch company data from Clearbit
useEffect(() => {
if (!companyDomain) return
// Only fetch if there's a company domain (i.e., company-specific URL)
if (!companyDomain) {
setIsLoading(false)
return
}
const fetchCompanyData = async () => {
// Check if companyDomain is actually a domain (contains a dot)
const isActualDomain = companyDomain.includes('.')
try {
const response = await fetch(`/api/customer?domain=${companyDomain}`)
if (response.ok) {
const data = await response.json()
setCompanyData({
name: data?.companyInfo?.name || companyDomain,
name: data?.companyInfo?.name || (isActualDomain ? companyDomain : ''),
domain: data?.companyInfo?.domain || companyDomain,
logo: data?.companyInfo?.logo || null,
logo: data?.companyInfo?.logo || undefined,
})
setSalesRep({
email: data?.accountManager?.email,
@@ -192,19 +236,19 @@ const CustomPresentationPage = () => {
color: data?.accountManager?.color,
})
} else {
// Fallback if Clearbit fails
// Fallback if API fails
setCompanyData({
name: companyDomain.replace('.com', '').replace('.io', ''),
name: isActualDomain ? companyDomain.replace('.com', '').replace('.io', '') : '',
domain: companyDomain,
logo: null,
logo: undefined,
})
}
} catch (error) {
console.error('Error fetching company data:', error)
setCompanyData({
name: companyDomain.replace('.com', '').replace('.io', ''),
name: isActualDomain ? companyDomain.replace('.com', '').replace('.io', '') : '',
domain: companyDomain,
logo: null,
logo: undefined,
})
} finally {
setIsLoading(false)
@@ -303,6 +347,7 @@ const CustomPresentationPage = () => {
companyLogo={props.companyLogo}
companyName={props.companyName}
salesRep={salesRep}
teamSlug={props.teamSlug}
slideKey={slideKey}
>
{props.children}
@@ -375,6 +420,8 @@ const CustomPresentationPage = () => {
)}
slides={slides}
presenterNotes={{}}
config={config.config}
salesRep={salesRep}
>
<div
data-scheme="primary"

View File

@@ -1,3 +1,116 @@
import ContactSales from 'components/ContactSales'
import { IconSend } from '@posthog/icons'
import React from 'react'
import ScrollArea from 'components/RadixUI/ScrollArea'
import SEO from 'components/seo'
export default ContactSales
const formConfig = {
type: 'lead' as const,
formOptions: {
className: 'pb-4 flex flex-col',
ctaLocation: 'top' as const,
showToField: true,
rowPadding: 'px-4',
},
form: {
ctaButton: {
label: 'Send',
size: 'md',
type: 'primary',
width: 'auto',
icon: <IconSend />,
},
message: "Message received! We'll be in touch.",
name: 'Contact sales',
fields: [
{
label: 'From',
placeholder: 'Your email',
type: 'string' as const,
name: 'email',
required: true,
fieldType: 'email',
},
{
label: 'Company',
type: 'string' as const,
name: 'company',
required: true,
},
{
label: 'Role',
name: 'role',
type: 'enumeration' as const,
options: [
{
label: 'Engineering',
value: 'Engineering',
},
{
label: 'Founder',
value: 'Founder',
},
{
label: 'Leadership',
value: 'Leadership',
},
{
label: 'Marketing',
value: 'Marketing',
},
{
label: 'Product',
value: 'Product',
},
{
label: 'Sales',
value: 'Sales',
},
{
label: 'Other',
value: 'Other',
},
],
required: true,
},
{
label: 'Monthly active users',
name: 'monthly_active_users',
type: 'string' as const,
fieldType: 'number',
required: true,
},
{
label: 'What do you want to talk about on the call?',
name: 'talk_about',
type: 'string' as const,
required: true,
fieldType: 'textarea',
hideLabel: true,
},
{
label: 'Where did you hear about us?',
type: 'string' as const,
name: 'where_did_you_hear_about_us',
required: false,
},
],
},
}
export default function TalkToAHuman() {
return (
<>
<SEO
title="Talk to a human Book a PostHog demo"
description="PostHog is self-serve, but our team is here if you need us. Book a demo to get setup help, discuss your technical requirements, or see features in action."
image={`/images/og/talk-to-a-human.png`}
/>
<ScrollArea>
<div data-scheme="primary" className="bg-accent text-primary h-full" data-default-form-id="509041">
<ContactSales formConfig={formConfig as any} />
</div>
</ScrollArea>
</>
)
}

View File

@@ -5,7 +5,7 @@
"template": "stacked",
"name": "Overview",
"title": "Product Engineers debug and ship faster with PostHog",
"description": "<p>By integrating PostHog into {companyName}, you'll be able to:</p><ul><li>track and diagnose errors</li><li>roll out and test new features</li><li>gain a better understanding of users</li></ul><p>Getting all of these capabilities through one SDK means you reduce the overhead of maintaining your app and can focus on shipping your product.</p>",
"description": "<p>By integrating PostHog into {companyName}, you can:</p><ul><li>track and diagnose errors</li><li>roll out and test new features</li><li>gain a better understanding of users</li></ul><p>Getting all of these capabilities through one SDK means you reduce the overhead of maintaining your app and can focus on shipping your product.</p>",
"descriptionWidth": "@2xl:w-3/5"
},
"error_tracking": {

View File

@@ -1,11 +1,16 @@
{
"name": "Product Engineers",
"config": {
"thumbnails": false,
"notes": false,
"form": true
},
"slides": {
"overview": {
"template": "stacked",
"name": "Overview",
"title": "Product Engineers debug and ship faster with PostHog",
"description": "<p>By integrating PostHog into {companyName}, you'll be able to:</p><ul><li>track and diagnose errors</li><li>roll out and test new features</li><li>gain a better understanding of users</li></ul><p>Getting all of these capabilities through one SDK means you reduce the overhead of maintaining your app and can focus on shipping your product.</p>",
"description": "<p>By integrating PostHog into {companyName}, you can:</p><ul><li>track and diagnose errors</li><li>roll out and test new features</li><li>gain a better understanding of users</li></ul><p>Getting all of these capabilities through one SDK means you reduce the overhead of maintaining your app and can focus on shipping your product.</p>",
"descriptionWidth": "@2xl:w-3/5"
},
"error_tracking": {

View File

@@ -24054,10 +24054,10 @@ postcss@^8.2.15, postcss@^8.2.9, postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.
picocolors "^1.1.1"
source-map-js "^1.2.1"
posthog-js@1.288.1:
version "1.288.1"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.288.1.tgz#a9d0e64be0132aa540a247ac39a4c0832e98c0ba"
integrity sha512-vDIbbtiLKehSZCwZhwfBQ67jxBBA4ah8yIjDkE1pK9dzWpIfpHf/3jbXyarU4Fu31KCL/z5KMufSvdMpF0tkkg==
posthog-js@1.290.0:
version "1.290.0"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.290.0.tgz#4181f0a56e7f4f0150d56bc8c783aaabb5297aa9"
integrity sha512-zavBwZkf+3JeiSDVE7ZDXBfzva/iOljicdhdJH+cZoqp0LsxjKxjnNhGOd3KpAhw0wqdwjhd7Lp1aJuI7DXyaw==
dependencies:
"@posthog/core" "1.5.2"
core-js "^3.38.1"