mirror of
https://github.com/BillyOutlast/posthog.com.git
synced 2026-02-04 03:11:21 +01:00
merged master
This commit is contained in:
@@ -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 />
|
||||
@@ -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 you’re 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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!).
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
12
contents/docs/integrate/_snippets/details/posthog-ips.mdx
Normal file
12
contents/docs/integrate/_snippets/details/posthog-ips.mdx
Normal 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 PostHog’s requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site.
|
||||
|
||||
**EU**: `3.75.65.221`, `18.197.246.42`, `3.120.223.253`
|
||||
|
||||
**US**: `44.205.89.55`, `52.4.194.122`, `44.208.188.173`
|
||||
|
||||
These are public, stable IPs used by PostHog services (e.g., Celery tasks for snapshots).
|
||||
|
||||
</details>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 />
|
||||
@@ -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).
|
||||
|
||||
@@ -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 />
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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¬es=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" />.
|
||||
|
||||

|
||||
|
||||
Company-specific landing pages show the <SmallTeam slug="sales-cs" /> by default.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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. We’ve 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 don’t 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.
|
||||
|
||||
We’re testing a **new process** for handling **product-led leads** that haven’t been acted on within **7 days**. If a lead is assigned to an AE but hasn’t 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 don’t 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 you’ve reviewed a lead and it’s 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 they’re 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 let’s 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
72
contents/handbook/marketing/customer-case-studies.md
Normal file
72
contents/handbook/marketing/customer-case-studies.md
Normal 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 there’s no one assigned in Vitally, you’re clear to go ahead and reach out directly.
|
||||
|
||||
Some customers have a dedicated Slack channel. If they do, that’s 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 you’ll need artwork later.
|
||||
|
||||
### 4. Schedule the interview
|
||||
Who you talk to for interviews doesn’t really matter. Speak to engineers, founders, PMs, or anyone who seems keen to chat. If you’re 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, it’s 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!
|
||||
|
||||
|
||||
@@ -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 don’t 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 haven’t 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 haven’t 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.
|
||||
|
||||
|
||||
@@ -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 individual’s 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 you’re 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 you’re 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 you’re celebrating!
|
||||
2. Select the third tab “MiniFig Creator” and design your mini fig to look like the individual you’re celebrating!
|
||||
3. Make sure to include a display case and the three tier brick option
|
||||
4. After you’ve completed your design, check out. There should already be a Brex card on file. Please make sure you add the individual’s 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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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, it’s where teams take action.
|
||||
|
||||
**From reactive to proactive**
|
||||
Analysis turns into action. Web analytics doesn’t 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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ interface PricingTemplateProps {
|
||||
textColor?: string
|
||||
companyLogo?: string
|
||||
companyName?: string
|
||||
salesRep?: SalesRep
|
||||
salesRep?: SalesRep | null
|
||||
slideKey?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}`}
|
||||
|
||||
168
src/components/Presentation/Utilities/PresentationForm.tsx
Normal file
168
src/components/Presentation/Utilities/PresentationForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
160
src/components/Presentation/Utilities/TeamMembers.tsx
Normal file
160
src/components/Presentation/Utilities/TeamMembers.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ export const sentry = {
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
session_replay: 'Limited',
|
||||
session_replay: true,
|
||||
product_analytics: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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[] } => {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
208
src/pages/discounts/index.tsx
Normal file
208
src/pages/discounts/index.tsx
Normal 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
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user