diff --git a/.github/workflows/ci-backend-depot.yml b/.github/workflows/ci-backend-depot.yml new file mode 100644 index 0000000000000..743fef1edaed1 --- /dev/null +++ b/.github/workflows/ci-backend-depot.yml @@ -0,0 +1,373 @@ +# This workflow runs all of our backend django tests. +# +# If these tests get too slow, look at increasing concurrency and re-timing the tests by manually dispatching +# .github/workflows/ci-backend-update-test-timing.yml action +name: Backend CI (depot) + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + inputs: + clickhouseServerVersion: + description: ClickHouse server version. Leave blank for default + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # This is so that the workflow run isn't canceled when a snapshot update is pushed within it by posthog-bot + # We do however cancel from container-images-ci.yml if a commit is pushed by someone OTHER than posthog-bot + cancel-in-progress: false + +env: + SECRET_KEY: '6b01eee4f945ca25045b5aab440b953461faf08693a9abbf1166dc7c6b9772da' # unsafe - for testing only + DATABASE_URL: 'postgres://posthog:posthog@localhost:5432/posthog' + REDIS_URL: 'redis://localhost' + CLICKHOUSE_HOST: 'localhost' + CLICKHOUSE_SECURE: 'False' + CLICKHOUSE_VERIFY: 'False' + TEST: 1 + CLICKHOUSE_SERVER_IMAGE_VERSION: ${{ github.event.inputs.clickhouseServerVersion || '' }} + OBJECT_STORAGE_ENABLED: 'True' + OBJECT_STORAGE_ENDPOINT: 'http://localhost:19000' + OBJECT_STORAGE_ACCESS_KEY_ID: 'object_storage_root_user' + OBJECT_STORAGE_SECRET_ACCESS_KEY: 'object_storage_root_password' + +jobs: + # Job to decide if we should run backend ci + # See https://github.com/dorny/paths-filter#conditional-execution for more details + changes: + runs-on: depot-ubuntu-latest + timeout-minutes: 5 + if: github.repository == 'PostHog/posthog' + name: Determine need to run backend checks + # Set job outputs to values from filter step + outputs: + backend: ${{ steps.filter.outputs.backend }} + steps: + # For pull requests it's not necessary to checkout the code, but we + # also want this to run on master so we need to checkout + - uses: actions/checkout@v3 + + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + backend: + # Avoid running backend tests for irrelevant changes + # NOTE: we are at risk of missing a dependency here. We could make + # the dependencies more clear if we separated the backend/frontend + # code completely + # really we should ignore ee/frontend/** but dorny doesn't support that + # - '!ee/frontend/**' + # including the negated rule appears to work + # but makes it always match because the checked file always isn't `ee/frontend/**` 🙈 + - 'ee/**/*' + - 'posthog/**/*' + - 'bin/*.py' + - requirements.txt + - requirements-dev.txt + - mypy.ini + - pytest.ini + - frontend/src/queries/schema.json # Used for generating schema.py + - plugin-transpiler/src # Used for transpiling plugins + # Make sure we run if someone is explicitly change the workflow + - .github/workflows/ci-backend.yml + - .github/actions/run-backend-tests/action.yml + # We use docker compose for tests, make sure we rerun on + # changes to docker-compose.dev.yml e.g. dependency + # version changes + - docker-compose.dev.yml + - frontend/public/email/* + # These scripts are used in the CI + - bin/check_temporal_up + - bin/check_kafka_clickhouse_up + + backend-code-quality: + needs: changes + timeout-minutes: 30 + + name: Python code quality checks + runs-on: depot-ubuntu-latest + + steps: + # If this run wasn't initiated by the bot (meaning: snapshot update) and we've determined + # there are backend changes, cancel previous runs + - uses: n1hility/cancel-previous-runs@v3 + if: github.actor != 'posthog-bot' && needs.changes.outputs.backend == 'true' + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.10.10 + cache: 'pip' + cache-dependency-path: '**/requirements*.txt' + token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} + + - uses: syphar/restore-virtualenv@v1 + id: cache-backend-tests + with: + custom_cache_key_element: v2- + + - name: Install SAML (python3-saml) dependencies + run: | + sudo apt-get update + sudo apt-get install libxml2-dev libxmlsec1 libxmlsec1-dev libxmlsec1-openssl + + - name: Install Python dependencies + if: steps.cache-backend-tests.outputs.cache-hit != 'true' + run: | + python -m pip install -r requirements.txt -r requirements-dev.txt + + - name: Check for syntax errors, import sort, and code style violations + run: | + ruff check . + + - name: Check formatting + run: | + ruff format --exclude posthog/hogql/grammar --check --diff . + + - name: Add Problem Matcher + run: echo "::add-matcher::.github/mypy-problem-matcher.json" + + - name: Check static typing + run: | + mypy -p posthog | mypy-baseline filter + + - name: Check if "schema.py" is up to date + run: | + npm run schema:build:python && git diff --exit-code + + - name: Check if ANTLR definitions are up to date + run: | + cd .. + sudo apt-get install default-jre + mkdir antlr + cd antlr + curl -o antlr.jar https://www.antlr.org/download/antlr-$ANTLR_VERSION-complete.jar + export PWD=`pwd` + echo '#!/bin/bash' > antlr + echo "java -jar $PWD/antlr.jar \$*" >> antlr + chmod +x antlr + export CLASSPATH=".:$PWD/antlr.jar:$CLASSPATH" + export PATH="$PWD:$PATH" + + cd ../posthog + antlr | grep "Version" + npm run grammar:build && git diff --exit-code + env: + # Installing a version of ANTLR compatible with what's in Homebrew as of October 2023 (version 4.13), + # as apt-get is quite out of date. The same version must be set in hogql_parser/pyproject.toml + ANTLR_VERSION: '4.13.1' + + check-migrations: + needs: changes + if: needs.changes.outputs.backend == 'true' + timeout-minutes: 10 + + name: Validate Django migrations + runs-on: depot-ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Stop/Start stack with Docker Compose + run: | + docker compose -f docker-compose.dev.yml down + docker compose -f docker-compose.dev.yml up -d + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.10.10 + cache: 'pip' + cache-dependency-path: '**/requirements*.txt' + token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} + + - uses: syphar/restore-virtualenv@v1 + id: cache-backend-tests + with: + custom_cache_key_element: v1- + + - name: Install SAML (python3-saml) dependencies + run: | + sudo apt-get update + sudo apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl + + - name: Install python dependencies + if: steps.cache-backend-tests.outputs.cache-hit != 'true' + run: | + python -m pip install -r requirements.txt -r requirements-dev.txt + + - uses: actions/checkout@v3 + with: + ref: master + + - name: Run migrations up to master + run: | + # We need to ensure we have requirements for the master branch + # now also, so we can run migrations up to master. + python -m pip install -r requirements.txt -r requirements-dev.txt + python manage.py migrate + + - uses: actions/checkout@v3 + + - name: Check migrations + run: | + python manage.py makemigrations --check --dry-run + git fetch origin master + # `git diff --name-only` returns a list of files that were changed - added OR deleted OR modified + # With `--name-status` we get the same, but including a column for status, respectively: A, D, M + # In this check we exclusively care about files that were + # added (A) in posthog/migrations/. We also want to ignore + # initial migrations (0001_*) as these are guaranteed to be + # run on initial setup where there is no data. + git diff --name-status origin/master..HEAD | grep "A\sposthog/migrations/" | awk '{print $2}' | grep -v migrations/0001_ | python manage.py test_migrations_are_safe + + django: + needs: changes + timeout-minutes: 30 + + name: Django tests – ${{ matrix.segment }} (persons-on-events ${{ matrix.person-on-events && 'on' || 'off' }}), Py ${{ matrix.python-version }}, ${{ matrix.clickhouse-server-image }} (${{matrix.group}}/${{ matrix.concurrency }}) + runs-on: depot-ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: ['3.10.10'] + clickhouse-server-image: ['clickhouse/clickhouse-server:23.11.2.11-alpine'] + segment: ['FOSS', 'EE'] + person-on-events: [false, true] + # :NOTE: Keep concurrency and groups in sync + concurrency: [5] + group: [1, 2, 3, 4, 5] + + steps: + # The first step is the only one that should run if `needs.changes.outputs.backend == 'false'`. + # All the other ones should rely on `needs.changes.outputs.backend` directly or indirectly, so that they're + # effectively skipped if backend code is unchanged. See https://github.com/PostHog/posthog/pull/15174. + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + # Use PostHog Bot token when not on forks to enable proper snapshot updating + token: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.POSTHOG_BOT_GITHUB_TOKEN || github.token }} + + - uses: ./.github/actions/run-backend-tests + if: needs.changes.outputs.backend == 'true' + with: + segment: ${{ matrix.segment }} + person-on-events: ${{ matrix.person-on-events }} + python-version: ${{ matrix.python-version }} + clickhouse-server-image: ${{ matrix.clickhouse-server-image }} + concurrency: ${{ matrix.concurrency }} + group: ${{ matrix.group }} + token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} + + - uses: EndBug/add-and-commit@v9 + # Skip on forks + # Also skip for persons-on-events runs, as we want to ignore snapshots diverging there + if: ${{ github.repository == 'PostHog/posthog' && needs.changes.outputs.backend == 'true' && !matrix.person-on-events }} + with: + add: '["ee", "./**/*.ambr", "posthog/queries/", "posthog/migrations", "posthog/tasks", "posthog/hogql/"]' + message: 'Update query snapshots' + pull: --rebase --autostash # Make sure we're up-to-date with other segments' updates + default_author: github_actions + github_token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} + + - name: Check if any snapshot changes were left uncomitted + id: changed-files + if: ${{ github.repository == 'PostHog/posthog' && needs.changes.outputs.backend == 'true' && !matrix.person-on-events }} + run: | + if [[ -z $(git status -s | grep -v ".test_durations" | tr -d "\n") ]] + then + echo 'files_found=false' >> $GITHUB_OUTPUT + else + echo 'diff=$(git status --porcelain)' >> $GITHUB_OUTPUT + echo 'files_found=true' >> $GITHUB_OUTPUT + fi + + - name: Fail CI if some snapshots have been updated but not committed + if: steps.changed-files.outputs.files_found == 'true' && steps.add-and-commit.outcome == 'success' + run: | + echo "${{ steps.changed-files.outputs.diff }}" + exit 1 + + - name: Archive email renders + uses: actions/upload-artifact@v3 + if: needs.changes.outputs.backend == 'true' && matrix.segment == 'FOSS' && matrix.person-on-events == false + with: + name: email_renders + path: posthog/tasks/test/__emails__ + retention-days: 5 + + async-migrations: + name: Async migrations tests - ${{ matrix.clickhouse-server-image }} + needs: changes + strategy: + fail-fast: false + matrix: + clickhouse-server-image: ['clickhouse/clickhouse-server:23.11.2.11-alpine'] + if: needs.changes.outputs.backend == 'true' + runs-on: depot-ubuntu-latest + steps: + - name: 'Checkout repo' + uses: actions/checkout@v3 + with: + fetch-depth: 1 + + - name: Start stack with Docker Compose + run: | + export CLICKHOUSE_SERVER_IMAGE_VERSION=${{ matrix.clickhouse-server-image }} + docker compose -f docker-compose.dev.yml down + docker compose -f docker-compose.dev.yml up -d + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.10.10 + cache: 'pip' + cache-dependency-path: '**/requirements*.txt' + token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} + + - uses: syphar/restore-virtualenv@v1 + id: cache-backend-tests + with: + custom_cache_key_element: v2- + + - name: Install SAML (python3-saml) dependencies + run: | + sudo apt-get update + sudo apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl + + - name: Install python dependencies + if: steps.cache-backend-tests.outputs.cache-hit != 'true' + shell: bash + run: | + python -m pip install -r requirements.txt -r requirements-dev.txt + + - name: Add kafka host to /etc/hosts for kafka connectivity + run: sudo echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts + + - name: Set up needed files + run: | + mkdir -p frontend/dist + touch frontend/dist/index.html + touch frontend/dist/layout.html + touch frontend/dist/exporter.html + + - name: Wait for Clickhouse & Kafka + run: bin/check_kafka_clickhouse_up + + - name: Run async migrations tests + run: | + pytest -m "async_migrations" diff --git a/.github/workflows/ci-e2e-depot.yml b/.github/workflows/ci-e2e-depot.yml new file mode 100644 index 0000000000000..615238bf7d43b --- /dev/null +++ b/.github/workflows/ci-e2e-depot.yml @@ -0,0 +1,278 @@ +# +# This workflow runs CI E2E tests with Cypress. +# +# It relies on the container image built by 'container-images-ci.yml'. +# +name: E2E CI (depot) + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: depot-ubuntu-latest + timeout-minutes: 5 + if: github.repository == 'PostHog/posthog' + name: Determine need to run E2E checks + # Set job outputs to values from filter step + outputs: + shouldTriggerCypress: ${{ steps.changes.outputs.shouldTriggerCypress }} + steps: + # For pull requests it's not necessary to check out the code + - uses: dorny/paths-filter@v2 + id: changes + with: + filters: | + shouldTriggerCypress: + # Avoid running E2E tests for irrelevant changes + # NOTE: we are at risk of missing a dependency here. We could make + # the dependencies more clear if we separated the backend/frontend + # code completely + - 'ee/**' + - 'posthog/**' + - 'hogvm/**' + - 'bin/*' + - frontend/**/* + - requirements.txt + - requirements-dev.txt + - package.json + - pnpm-lock.yaml + # Make sure we run if someone is explicitly change the workflow + - .github/workflows/ci-e2e.yml + - .github/actions/build-n-cache-image/action.yml + # We use docker compose for tests, make sure we rerun on + # changes to docker-compose.dev.yml e.g. dependency + # version changes + - docker-compose.dev.yml + - Dockerfile + - cypress/** + + # Job that lists and chunks spec file names and caches node modules + chunks: + needs: changes + name: Cypress preparation + runs-on: depot-ubuntu-latest + timeout-minutes: 5 + outputs: + chunks: ${{ steps.chunk.outputs.chunks }} + + steps: + - name: Check out + uses: actions/checkout@v3 + + - name: Group spec files into chunks of three + id: chunk + run: echo "chunks=$(ls cypress/e2e/* | jq --slurp --raw-input -c 'split("\n")[:-1] | _nwise(2) | join("\n")' | jq --slurp -c .)" >> $GITHUB_OUTPUT + + container: + name: Build and cache container image + runs-on: depot-ubuntu-latest + timeout-minutes: 60 + needs: [changes] + permissions: + contents: read + id-token: write # allow issuing OIDC tokens for this workflow run + outputs: + tag: ${{ steps.build.outputs.tag }} + build-id: ${{ steps.build.outputs.build-id }} + steps: + - name: Checkout + if: needs.changes.outputs.shouldTriggerCypress == 'true' + uses: actions/checkout@v3 + - name: Build the Docker image with Depot + if: needs.changes.outputs.shouldTriggerCypress == 'true' + # Build the container image in preparation for the E2E tests + uses: ./.github/actions/build-n-cache-image + id: build + with: + save: true + actions-id-token-request-url: ${{ env.ACTIONS_ID_TOKEN_REQUEST_URL }} + + cypress: + name: Cypress E2E tests (${{ strategy.job-index }}) + runs-on: depot-ubuntu-latest + timeout-minutes: 60 + needs: [chunks, changes, container] + permissions: + id-token: write # allow issuing OIDC tokens for this workflow run + + strategy: + # when one test fails, DO NOT cancel the other + # containers, as there may be other spec failures + # we want to know about. + fail-fast: false + matrix: + chunk: ${{ fromJson(needs.chunks.outputs.chunks) }} + + steps: + - name: Checkout + if: needs.changes.outputs.shouldTriggerCypress == 'true' + uses: actions/checkout@v3 + + - name: Install pnpm + if: needs.changes.outputs.shouldTriggerCypress == 'true' + uses: pnpm/action-setup@v2 + with: + version: 8.x.x + + - name: Set up Node.js + if: needs.changes.outputs.shouldTriggerCypress == 'true' + uses: actions/setup-node@v4 + with: + node-version: 18.12.1 + + - name: Get pnpm cache directory path + if: needs.changes.outputs.shouldTriggerCypress == 'true' + id: pnpm-cache-dir + run: echo "PNPM_STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Get cypress cache directory path + if: needs.changes.outputs.shouldTriggerCypress == 'true' + id: cypress-cache-dir + run: echo "CYPRESS_BIN_PATH=$(npx cypress cache path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v4 + if: needs.changes.outputs.shouldTriggerCypress == 'true' + id: pnpm-cache + with: + path: | + ${{ steps.pnpm-cache-dir.outputs.PNPM_STORE_PATH }} + ${{ steps.cypress-cache-dir.outputs.CYPRESS_BIN_PATH }} + key: ${{ runner.os }}-pnpm-cypress-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-cypress- + + - name: Install package.json dependencies with pnpm + if: needs.changes.outputs.shouldTriggerCypress == 'true' + run: pnpm install --frozen-lockfile + + - name: Stop/Start stack with Docker Compose + # these are required checks so, we can't skip entire sections + if: needs.changes.outputs.shouldTriggerCypress == 'true' + run: | + docker compose -f docker-compose.dev.yml down + docker compose -f docker-compose.dev.yml up -d + + - name: Wait for ClickHouse + # these are required checks so, we can't skip entire sections + if: needs.changes.outputs.shouldTriggerCypress == 'true' + run: ./bin/check_kafka_clickhouse_up + + - name: Install Depot CLI + if: needs.changes.outputs.shouldTriggerCypress == 'true' + uses: depot/setup-action@v1 + + - name: Get Docker image cached in Depot + if: needs.changes.outputs.shouldTriggerCypress == 'true' + uses: depot/pull-action@v1 + with: + build-id: ${{ needs.container.outputs.build-id }} + tags: ${{ needs.container.outputs.tag }} + + - name: Write .env # This step intentionally has no if, so that GH always considers the action as having run + run: | + cat <> .env + SECRET_KEY=6b01eee4f945ca25045b5aab440b953461faf08693a9abbf1166dc7c6b9772da + REDIS_URL=redis://localhost + DATABASE_URL=postgres://posthog:posthog@localhost:5432/posthog + KAFKA_HOSTS=kafka:9092 + DISABLE_SECURE_SSL_REDIRECT=1 + SECURE_COOKIES=0 + OPT_OUT_CAPTURE=0 + E2E_TESTING=1 + SKIP_SERVICE_VERSION_REQUIREMENTS=1 + EMAIL_HOST=email.test.posthog.net + SITE_URL=http://localhost:8000 + NO_RESTART_LOOP=1 + CLICKHOUSE_SECURE=0 + OBJECT_STORAGE_ENABLED=1 + OBJECT_STORAGE_ENDPOINT=http://localhost:19000 + OBJECT_STORAGE_ACCESS_KEY_ID=object_storage_root_user + OBJECT_STORAGE_SECRET_ACCESS_KEY=object_storage_root_password + GITHUB_ACTION_RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + CELERY_METRICS_PORT=8999 + EOT + + - name: Start PostHog + # these are required checks so, we can't skip entire sections + if: needs.changes.outputs.shouldTriggerCypress == 'true' + run: | + mkdir -p /tmp/logs + + echo "Starting PostHog using the container image ${{ needs.container.outputs.tag }}" + DOCKER_RUN="docker run --rm --network host --add-host kafka:127.0.0.1 --env-file .env ${{ needs.container.outputs.tag }}" + + $DOCKER_RUN ./bin/migrate + $DOCKER_RUN python manage.py setup_dev + + # only starts the plugin server so that the "wait for PostHog" step passes + $DOCKER_RUN ./bin/docker-worker &> /tmp/logs/worker.txt & + $DOCKER_RUN ./bin/docker-server &> /tmp/logs/server.txt & + + - name: Wait for PostHog + # these are required checks so, we can't skip entire sections + if: needs.changes.outputs.shouldTriggerCypress == 'true' + # this action might be abandoned - but v1 doesn't point to latest of v1 (which it should) + # so pointing to v1.1.0 to remove warnings about node version with v1 + # todo check https://github.com/iFaxity/wait-on-action/releases for new releases + uses: iFaxity/wait-on-action@v1.1.0 + timeout-minutes: 3 + with: + verbose: true + log: true + resource: http://localhost:8000 + + - name: Cypress run + # these are required checks so, we can't skip entire sections + if: needs.changes.outputs.shouldTriggerCypress == 'true' + uses: cypress-io/github-action@v6 + with: + config-file: cypress.e2e.config.ts + config: retries=2 + spec: ${{ matrix.chunk }} + install: false + env: + E2E_TESTING: 1 + OPT_OUT_CAPTURE: 0 + GITHUB_ACTION_RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + + - name: Archive test screenshots + uses: actions/upload-artifact@v3 + with: + name: screenshots + path: cypress/screenshots + if: ${{ failure() }} + + - name: Archive test downloads + uses: actions/upload-artifact@v3 + with: + name: downloads + path: cypress/downloads + if: ${{ failure() }} + + - name: Archive test videos + uses: actions/upload-artifact@v3 + with: + name: videos + path: cypress/videos + if: ${{ failure() }} + + - name: Archive accessibility violations + if: needs.changes.outputs.shouldTriggerCypress == 'true' + uses: actions/upload-artifact@v3 + with: + name: accessibility-violations + path: '**/a11y/' + if-no-files-found: 'ignore' + + - name: Show logs on failure + # use artefact here, as I think the output will be too large for display in an action + uses: actions/upload-artifact@v3 + with: + name: logs-${{ strategy.job-index }} + path: /tmp/logs + if: ${{ failure() }} diff --git a/cypress/e2e/billingUpgradeCTA.cy.ts b/cypress/e2e/billingUpgradeCTA.cy.ts new file mode 100644 index 0000000000000..18504d2fe6a18 --- /dev/null +++ b/cypress/e2e/billingUpgradeCTA.cy.ts @@ -0,0 +1,57 @@ +import { decideResponse } from '../fixtures/api/decide' +import * as fflate from 'fflate' + +// Mainly testing to make sure events are fired as expected + +describe('Billing Upgrade CTA', () => { + beforeEach(() => { + cy.intercept('**/decide/*', (req) => + req.reply( + decideResponse({ + 'billing-upgrade-language': 'credit_card', + }) + ) + ) + + cy.intercept('/api/billing-v2/', { fixture: 'api/billing-v2/billing-v2-unsubscribed.json' }) + }) + + it('Check that events are being sent on each page visit', () => { + cy.visit('/organization/billing') + cy.get('[data-attr=product_analytics-upgrade-cta] .LemonButton__content').should('have.text', 'Add credit card') + cy.window().then((win) => { + const events = (win as any)._cypress_posthog_captures + + const matchingEvents = events.filter((event) => event.event === 'billing CTA shown') + // One for each product card + expect(matchingEvents.length).to.equal(4) + }) + + // Mock billing response with subscription + cy.intercept('/api/billing-v2/', { fixture: 'api/billing-v2/billing-v2.json' }) + cy.reload() + + cy.get('[data-attr=session_replay-upgrade-cta] .LemonButton__content').should('have.text', 'Add paid plan') + cy.intercept('POST', '**/e/?compression=gzip-js*').as('capture3') + cy.window().then((win) => { + const events = (win as any)._cypress_posthog_captures + + const matchingEvents = events.filter((event) => event.event === 'billing CTA shown') + expect(matchingEvents.length).to.equal(3) + }) + + cy.intercept('/api/billing-v2/', { fixture: 'api/billing-v2/billing-v2-unsubscribed.json' }) + // Navigate to the onboarding billing step + cy.visit('/products') + cy.get('[data-attr=product_analytics-onboarding-card]').click() + cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(5)').click() + + cy.intercept('POST', '**/e/?compression=gzip-js*').as('capture4') + cy.window().then((win) => { + const events = (win as any)._cypress_posthog_captures + + const matchingEvents = events.filter((event) => event.event === 'billing CTA shown') + expect(matchingEvents.length).to.equal(3) + }) + }) +}) diff --git a/cypress/e2e/billingv2.cy.ts b/cypress/e2e/billingv2.cy.ts index 13b819a441317..12db2073a1202 100644 --- a/cypress/e2e/billingv2.cy.ts +++ b/cypress/e2e/billingv2.cy.ts @@ -14,7 +14,7 @@ describe('Billing', () => { cy.get('[data-attr=more-button]').first().click() cy.contains('.LemonButton', 'Unsubscribe').click() - cy.get('.LemonModal h3').should('contain', 'Why are you unsubscribing from Product analytics + data stack?') + cy.get('.LemonModal h3').should('contain', 'Why are you unsubscribing from Product analytics?') cy.get('[data-attr=unsubscribe-reason-survey-textarea]').type('Product analytics') cy.contains('.LemonModal .LemonButton', 'Unsubscribe').click() @@ -35,7 +35,7 @@ describe('Billing', () => { it('Unsubscribe survey text area maintains unique state between product types', () => { cy.get('[data-attr=more-button]').first().click() cy.contains('.LemonButton', 'Unsubscribe').click() - cy.get('.LemonModal h3').should('contain', 'Why are you unsubscribing from Product analytics + data stack?') + cy.get('.LemonModal h3').should('contain', 'Why are you unsubscribing from Product analytics?') cy.get('[data-attr=unsubscribe-reason-survey-textarea]').type('Product analytics') cy.contains('.LemonModal .LemonButton', 'Cancel').click() diff --git a/cypress/e2e/onboarding.cy.ts b/cypress/e2e/onboarding.cy.ts index ecf7b86154b96..56cb23bc45b70 100644 --- a/cypress/e2e/onboarding.cy.ts +++ b/cypress/e2e/onboarding.cy.ts @@ -3,6 +3,8 @@ import { decideResponse } from '../fixtures/api/decide' describe('Onboarding', () => { beforeEach(() => { + cy.intercept('/api/billing-v2/', { fixture: 'api/billing-v2/billing-v2-unsubscribed.json' }) + cy.intercept('**/decide/*', (req) => req.reply( decideResponse({ @@ -21,24 +23,158 @@ describe('Onboarding', () => { // Confirm product intro is not included as the first step in the upper right breadcrumbs cy.get('[data-attr=onboarding-breadcrumbs] > :first-child > * span').should('not.contain', 'Product intro') - // Navigate to the product intro page by clicking the left side bar - cy.get('[data-attr=menu-item-replay').click() + cy.window().then((win) => { + win.POSTHOG_APP_CONTEXT.current_team.has_completed_onboarding_for = {} + }) + + cy.get('[data-attr=menu-item-savedinsights]').click() // Confirm we're on the product_intro page cy.get('[data-attr=top-bar-name] > span').contains('Onboarding') - cy.get('[data-attr=product-intro-title]').contains('Watch how users experience your app') + cy.get('[data-attr=product-intro-title]').contains('Product analytics with autocapture') - // Go back to /products - cy.visit('/products') + cy.get('[data-attr=start-onboarding]').should('be.visible') + cy.get('[data-attr=skip-onboarding]').should('not.exist') + }) - // Again get started on product analytics onboarding - cy.get('[data-attr=product_analytics-onboarding-card]').click() + // it('Step through PA onboarding', () => { + // cy.visit('/products') - // Navigate to the product intro page by changing the url - cy.visit(urls.onboarding('session_replay', 'product_intro')) + // // Get started on product analytics onboarding + // cy.get('[data-attr=product_analytics-onboarding-card]').click() - // Confirm we're on the product intro page - cy.get('[data-attr=top-bar-name] > span').contains('Onboarding') - cy.get('[data-attr=product-intro-title]').contains('Watch how users experience your app') - }) + // // Installation should be complete + // cy.get('svg.LemonIcon.text-success').should('exist') + // cy.get('svg.LemonIcon.text-success').parent().should('contain', 'Installation complete') + + // // Continue to configuration step + // cy.get('[data-attr=sdk-continue]').click() + + // // Confirm the appropriate breadcrumb is highlighted + // cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(3) > * span').should('contain', 'Configure') + // cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(3) > * span').should('not.have.css', 'text-muted') + + // // Continue to plans + // cy.get('[data-attr=onboarding-continue]').click() + + // // Verify pricing table visible + // cy.get('.BillingHero').should('be.visible') + // cy.get('table.PlanComparison').should('be.visible') + + // // Confirm buttons on pricing comparison + // cy.get('[data-attr=upgrade-Paid] .LemonButton__content').should('have.text', 'Upgrade') + // cy.get('[data-attr=upgrade-Free] .LemonButton__content').should('have.text', 'Current plan') + + // // Continue + // cy.get('[data-attr=onboarding-skip-button]').click() + + // // Click back to Install step + // cy.get('[data-attr=onboarding-breadcrumbs] > :first-child > * span').click() + + // // Continue through to finish + // cy.get('[data-attr=sdk-continue]').click() + // cy.get('[data-attr=onboarding-continue]').click() + // cy.get('[data-attr=onboarding-skip-button]').click() + // cy.get('[data-attr=onboarding-continue]').click() + + // // Confirm we're on the insights list page + // cy.url().should('contain', 'project/1/insights') + + // cy.visit('/onboarding/product_analytics?step=product_intro') + + // // Should see both an option to skip onboarding and an option to see the sdk instructions + // cy.get('[data-attr=skip-onboarding]').should('be.visible') + // cy.get('[data-attr=start-onboarding-sdk]').should('be.visible') + + // cy.get('[data-attr=skip-onboarding]').first().click() + // cy.url().should('contain', 'project/1/insights') + + // cy.visit('/onboarding/product_analytics?step=product_intro') + // cy.get('[data-attr=start-onboarding-sdk]').first().click() + // cy.url().should('contain', 'project/1/onboarding/product_analytics?step=install') + + // cy.visit('/products') + // cy.get('[data-attr=return-to-product_analytics] > svg').click() + // cy.url().should('contain', 'project/1/insights') + // }) + + // it('Step through SR onboarding', () => { + // cy.visit('/products') + // cy.get('[data-attr=session_replay-onboarding-card]').click() + + // // Installation should be complete + // cy.get('svg.LemonIcon.text-success').should('exist') + // cy.get('svg.LemonIcon.text-success').parent().should('contain', 'Installation complete') + // // Continue to configuration step + // cy.get('[data-attr=sdk-continue]').click() + // // Continue to plans + // cy.get('[data-attr=onboarding-continue]').click() + // // Verify pricing table visible + // cy.get('.BillingHero').should('be.visible') + // cy.get('table.PlanComparison').should('be.visible') + // // Confirm buttons on pricing comparison + // cy.get('[data-attr=upgrade-Paid] .LemonButton__content').should('have.text', 'Upgrade') + // cy.get('[data-attr=upgrade-Free] .LemonButton__content').should('have.text', 'Current plan') + // // Continue through to finish + // cy.get('[data-attr=onboarding-skip-button]').click() + // cy.get('[data-attr=onboarding-continue]').click() + // // Confirm we're on the recordings list page + // cy.url().should('contain', 'project/1/replay/recent') + // cy.visit('/onboarding/session_replay?step=product_intro') + // cy.get('[data-attr=skip-onboarding]').should('be.visible') + // cy.get('[data-attr=start-onboarding-sdk]').should('not.exist') + // }) + + // it('Step through FF onboarding', () => { + // cy.visit('/onboarding/feature_flags?step=product_intro') + // cy.get('[data-attr=start-onboarding-sdk]').first().click() + // cy.get('[data-attr=sdk-continue]').click() + + // // Confirm the appropriate breadcrumb is highlighted + // cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(5) > * span').should('contain', 'Plans') + // cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(3) > * span').should('not.have.css', 'text-muted') + + // cy.get('[data-attr=onboarding-skip-button]').click() + // cy.get('[data-attr=onboarding-continue]').click() + + // cy.url().should('contain', '/feature_flags') + + // cy.visit('/onboarding/feature_flags?step=product_intro') + + // cy.get('[data-attr=skip-onboarding]').should('be.visible') + // cy.get('[data-attr=start-onboarding-sdk]').should('be.visible') + + // cy.get('[data-attr=skip-onboarding]').first().click() + // }) + + // it('Step through Surveys onboarding', () => { + // cy.visit('/onboarding/surveys?step=product_intro') + // cy.get('[data-attr=skip-onboarding]').should('be.visible') + // cy.get('[data-attr=start-onboarding-sdk]').should('not.exist') + // cy.get('[data-attr=skip-onboarding]').first().click() + // cy.url().should('contain', 'survey_templates') + + // cy.visit('/products') + // cy.get('[data-attr=surveys-onboarding-card]').click() + // // Installation should be complete + // cy.get('svg.LemonIcon.text-success').should('exist') + // cy.get('svg.LemonIcon.text-success').parent().should('contain', 'Installation complete') + + // // Continue to configuration step + // cy.get('[data-attr=sdk-continue]').click() + + // // Verify pricing table visible + // cy.get('.BillingHero').should('be.visible') + // cy.get('table.PlanComparison').should('be.visible') + + // // Confirm buttons on pricing comparison + // cy.get('[data-attr=upgrade-Paid] .LemonButton__content').should('have.text', 'Upgrade') + // cy.get('[data-attr=upgrade-Free] .LemonButton__content').should('have.text', 'Current plan') + + // // Continue + // cy.get('[data-attr=onboarding-skip-button]').click() + // cy.get('[data-attr=onboarding-continue]').click() + + // cy.url().should('contain', '/survey_templates') + // }) }) diff --git a/cypress/fixtures/api/billing-v2/billing-v2-unsubscribed.json b/cypress/fixtures/api/billing-v2/billing-v2-unsubscribed.json index 60aed5c9693c4..32e1268032d34 100644 --- a/cypress/fixtures/api/billing-v2/billing-v2-unsubscribed.json +++ b/cypress/fixtures/api/billing-v2/billing-v2-unsubscribed.json @@ -14,6 +14,7 @@ "discord_integration", "apps", "boolean_flags", + "multivariate_flags", "persist_flags_cross_authentication", "feature_flag_payloads", "multiple_release_conditions", @@ -21,6 +22,10 @@ "targeting_by_group", "local_evaluation_and_bootstrapping", "flag_usage_stats", + "experimentation", + "funnel_experiments", + "secondary_metrics", + "statistical_analysis", "feature_flags_data_retention", "console_logs", "recordings_performance", @@ -47,17 +52,17 @@ "api_access", "social_sso", "community_support", - "terms_and_conditions" + "2fa" ], "license": { - "plan": "cloud" + "plan": "dev" }, - "customer_id": null, + "customer_id": "cus_Pg7PIL8MsKi6bx", "deactivated": false, "has_active_subscription": false, "billing_period": { - "current_period_start": "2024-02-06T19:37:14.843Z", - "current_period_end": "2024-03-07T19:37:14.843Z", + "current_period_start": "2024-03-04T23:43:35.772Z", + "current_period_end": "2024-04-03T23:43:35.772Z", "interval": "month" }, "available_product_features": [ @@ -173,6 +178,14 @@ "limit": null, "note": null }, + { + "key": "multivariate_flags", + "name": "Multivariate feature flags & experiments", + "description": "Create three or more variants of a feature flag to test or release different versions of a feature.", + "unit": null, + "limit": null, + "note": null + }, { "key": "persist_flags_cross_authentication", "name": "Persist flags across authentication", @@ -229,6 +242,38 @@ "limit": null, "note": null }, + { + "key": "experimentation", + "name": "A/B testing", + "description": "Test changes to your product and evaluate the impacts those changes make.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "funnel_experiments", + "name": "Funnel & trend experiments", + "description": "Measure the impact of a change on a aggregate values or a series of events, like a signup flow.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "secondary_metrics", + "name": "Secondary experiment metrics", + "description": "Track additional metrics to see how your experiment affects other parts of your app or different flows.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "statistical_analysis", + "name": "Statistical analysis", + "description": "Get a statistical analysis of your experiment results to see if the results are significant, or if they're likely just due to chance.", + "unit": null, + "limit": null, + "note": null + }, { "key": "feature_flags_data_retention", "name": "Data retention", @@ -438,12 +483,12 @@ "note": null }, { - "key": "terms_and_conditions", - "name": "Terms and conditions", - "description": "Terms and conditions", + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", "unit": null, "limit": null, - "note": "Standard" + "note": null } ], "current_total_amount_usd": null, @@ -464,7 +509,7 @@ { "plan_key": "free-20230117", "product_key": "product_analytics", - "name": "Product analytics", + "name": "Free", "description": "A comprehensive product analytics platform built to natively work with session replay, feature flags, A/B testing, and surveys.", "image_url": "https://posthog.com/images/products/product-analytics/product-analytics.png", "docs_url": "https://posthog.com/docs/product-analytics", @@ -515,12 +560,14 @@ ], "tiers": null, "current_plan": true, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null }, { "plan_key": "paid-20240111", "product_key": "product_analytics", - "name": "Product analytics", + "name": "Paid", "description": "A comprehensive product analytics platform built to natively work with session replay, feature flags, A/B testing, and surveys.", "image_url": "https://posthog.com/images/products/product-analytics/product-analytics.png", "docs_url": "https://posthog.com/docs/product-analytics", @@ -576,30 +623,6 @@ "limit": null, "note": null }, - { - "key": "dashboard_permissioning", - "name": "Dashboard permissions", - "description": "Restrict access to dashboards within the organization to only those who need it.", - "unit": null, - "limit": null, - "note": null - }, - { - "key": "dashboard_collaboration", - "name": "Tags & text cards", - "description": "Keep organized by adding tags to your dashboards, cohorts and more. Add text cards and descriptions to your dashboards to provide context to your team.", - "unit": null, - "limit": null, - "note": null - }, - { - "key": "ingestion_taxonomy", - "name": "Ingestion taxonomy", - "description": "Ingestion taxonomy", - "unit": null, - "limit": null, - "note": null - }, { "key": "correlation_analysis", "name": "Correlation analysis", @@ -608,14 +631,6 @@ "limit": null, "note": null }, - { - "key": "tagging", - "name": "Dashboard tags", - "description": "Organize dashboards with tags.", - "unit": null, - "limit": null, - "note": null - }, { "key": "behavioral_cohort_filtering", "name": "Lifecycle", @@ -645,7 +660,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000248", + "unit_amount_usd": "0.00031", "up_to": 2000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -654,7 +669,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000104", + "unit_amount_usd": "0.00013", "up_to": 15000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -663,7 +678,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000655", + "unit_amount_usd": "0.0000819", "up_to": 50000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -672,7 +687,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000364", + "unit_amount_usd": "0.0000455", "up_to": 100000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -681,7 +696,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000187", + "unit_amount_usd": "0.0000234", "up_to": 250000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -690,7 +705,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000042", + "unit_amount_usd": "0.0000052", "up_to": null, "current_amount_usd": "0.00", "current_usage": 0, @@ -699,7 +714,9 @@ } ], "current_plan": false, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], "type": "product_analytics", @@ -737,7 +754,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000071", + "unit_amount_usd": "0.0000708", "up_to": 2000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -803,7 +820,7 @@ { "plan_key": "addon-20230509", "product_key": "group_analytics", - "name": "Group analytics", + "name": "Addon", "description": "Associate events with a group or entity - such as a company, community, or project. Analyze these events as if they were sent by that entity itself. Great for B2B, marketplaces, and more.", "image_url": "https://posthog.com/images/product/product-icons/group-analytics.svg", "docs_url": "https://posthog.com/docs/product-analytics/group-analytics", @@ -832,7 +849,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000071", + "unit_amount_usd": "0.0000708", "up_to": 2000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -886,10 +903,12 @@ } ], "current_plan": false, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], - "contact_support": false + "contact_support": null }, { "name": "Data pipelines", @@ -911,7 +930,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000062", + "unit_amount_usd": "0.000248", "up_to": 2000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -920,7 +939,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000026", + "unit_amount_usd": "0.000104", "up_to": 15000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -929,7 +948,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000164", + "unit_amount_usd": "0.0000655", "up_to": 50000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -938,7 +957,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000091", + "unit_amount_usd": "0.0000364", "up_to": 100000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -947,7 +966,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000047", + "unit_amount_usd": "0.0000187", "up_to": 250000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -956,7 +975,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000001", + "unit_amount_usd": "0.0000042", "up_to": null, "current_amount_usd": "0.00", "current_usage": 0, @@ -977,7 +996,7 @@ { "plan_key": "addon-20240111", "product_key": "data_pipelines", - "name": "Data pipelines", + "name": "Addon", "description": "Get your PostHog data into your data warehouse or other tools like BigQuery, Redshift, Customer.io, and more.", "image_url": null, "docs_url": "https://posthog.com/docs/cdp/batch-exports", @@ -1006,7 +1025,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000062", + "unit_amount_usd": "0.000248", "up_to": 2000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -1015,7 +1034,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000026", + "unit_amount_usd": "0.000104", "up_to": 15000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -1024,7 +1043,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000164", + "unit_amount_usd": "0.0000655", "up_to": 50000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -1033,7 +1052,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000091", + "unit_amount_usd": "0.0000364", "up_to": 100000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -1042,7 +1061,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0000047", + "unit_amount_usd": "0.0000187", "up_to": 250000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -1051,7 +1070,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000001", + "unit_amount_usd": "0.0000042", "up_to": null, "current_amount_usd": "0.00", "current_usage": 0, @@ -1060,10 +1079,12 @@ } ], "current_plan": false, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], - "contact_support": false + "contact_support": null } ], "contact_support": false, @@ -1131,30 +1152,6 @@ "icon_key": "IconNotification", "type": "secondary" }, - { - "key": "dashboard_collaboration", - "name": "Tags & text cards", - "description": "Keep organized by adding tags to your dashboards, cohorts and more. Add text cards and descriptions to your dashboards to provide context to your team.", - "images": null, - "icon_key": null, - "type": null - }, - { - "key": "dashboard_permissioning", - "name": "Dashboard permissions", - "description": "Restrict access to dashboards within the organization to only those who need it.", - "images": null, - "icon_key": null, - "type": null - }, - { - "key": "ingestion_taxonomy", - "name": "Ingestion taxonomy", - "description": "Ingestion taxonomy", - "images": null, - "icon_key": null, - "type": null - }, { "key": "paths_advanced", "name": "Advanced paths", @@ -1174,14 +1171,6 @@ "icon_key": null, "type": "primary" }, - { - "key": "tagging", - "name": "Dashboard tags", - "description": "Organize dashboards with tags.", - "images": null, - "icon_key": null, - "type": null - }, { "key": "behavioral_cohort_filtering", "name": "Lifecycle", @@ -1256,7 +1245,7 @@ { "plan_key": "free-20231218", "product_key": "session_replay", - "name": "Session replay", + "name": "Free", "description": "Session replay helps you diagnose issues and understand user behavior in your product or website.", "image_url": "https://posthog.com/images/products/session-replay/session-replay.png", "docs_url": "https://posthog.com/docs/session-replay", @@ -1379,12 +1368,14 @@ ], "tiers": null, "current_plan": true, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null }, { "plan_key": "paid-20231218", "product_key": "session_replay", - "name": "Session replay", + "name": "Paid", "description": "Session replay helps you diagnose issues and understand user behavior in your product or website.", "image_url": "https://posthog.com/images/products/session-replay/session-replay.png", "docs_url": "https://posthog.com/docs/session-replay", @@ -1570,7 +1561,9 @@ } ], "current_plan": false, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], "type": "session_replay", @@ -1753,7 +1746,7 @@ { "plan_key": "free-20230117", "product_key": "feature_flags", - "name": "Feature flags & A/B testing", + "name": "Free", "description": "Test changes with small groups of users before rolling out wider. Analyze usage with product analytics and session replay.", "image_url": "https://posthog.com/images/products/feature-flags/feature-flags.png", "docs_url": "https://posthog.com/docs/feature-flags", @@ -1769,6 +1762,14 @@ "limit": null, "note": null }, + { + "key": "multivariate_flags", + "name": "Multivariate feature flags & experiments", + "description": "Create three or more variants of a feature flag to test or release different versions of a feature.", + "unit": null, + "limit": null, + "note": null + }, { "key": "persist_flags_cross_authentication", "name": "Persist flags across authentication", @@ -1825,6 +1826,38 @@ "limit": null, "note": null }, + { + "key": "experimentation", + "name": "A/B testing", + "description": "Test changes to your product and evaluate the impacts those changes make.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "funnel_experiments", + "name": "Funnel & trend experiments", + "description": "Measure the impact of a change on a aggregate values or a series of events, like a signup flow.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "secondary_metrics", + "name": "Secondary experiment metrics", + "description": "Track additional metrics to see how your experiment affects other parts of your app or different flows.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "statistical_analysis", + "name": "Statistical analysis", + "description": "Get a statistical analysis of your experiment results to see if the results are significant, or if they're likely just due to chance.", + "unit": null, + "limit": null, + "note": null + }, { "key": "feature_flags_data_retention", "name": "Data retention", @@ -1836,12 +1869,14 @@ ], "tiers": null, "current_plan": true, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null }, { "plan_key": "paid-20230623", "product_key": "feature_flags", - "name": "Feature flags & A/B testing", + "name": "Paid", "description": "Test changes with small groups of users before rolling out wider. Analyze usage with product analytics and session replay.", "image_url": "https://posthog.com/images/products/feature-flags/feature-flags.png", "docs_url": "https://posthog.com/docs/feature-flags", @@ -1929,14 +1964,6 @@ "limit": null, "note": null }, - { - "key": "group_experiments", - "name": "Group experiments", - "description": "Target experiments to specific groups of users so everyone in the same group gets the same variant.", - "unit": null, - "limit": null, - "note": null - }, { "key": "funnel_experiments", "name": "Funnel & trend experiments", @@ -1961,6 +1988,22 @@ "limit": null, "note": null }, + { + "key": "group_experiments", + "name": "Group experiments", + "description": "Target experiments to specific groups of users so everyone in the same group gets the same variant.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "multiple_environments", + "name": "Multi-environment support", + "description": "Test flags in local development or staging by using the same flag key across PostHog projects.", + "unit": null, + "limit": null, + "note": null + }, { "key": "feature_flags_data_retention", "name": "Data retention", @@ -2018,7 +2061,9 @@ } ], "current_plan": false, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], "type": "feature_flags", @@ -2215,7 +2260,7 @@ { "plan_key": "free-20230928", "product_key": "surveys", - "name": "Surveys", + "name": "Free", "description": "Build in-app popups with freeform text responses, multiple choice, NPS, ratings, and emoji reactions. Or use the API for complete control.", "image_url": "https://posthog.com/images/products/surveys/surveys.png", "docs_url": "https://posthog.com/docs/surveys", @@ -2290,12 +2335,14 @@ ], "tiers": null, "current_plan": true, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null }, { "plan_key": "paid-20230928", "product_key": "surveys", - "name": "Surveys", + "name": "Paid", "description": "Build in-app popups with freeform text responses, multiple choice, NPS, ratings, and emoji reactions. Or use the API for complete control.", "image_url": "https://posthog.com/images/products/surveys/surveys.png", "docs_url": "https://posthog.com/docs/surveys", @@ -2449,7 +2496,9 @@ } ], "current_plan": false, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], "type": "surveys", @@ -2617,7 +2666,7 @@ { "plan_key": "free-20230117", "product_key": "integrations", - "name": "Integrations", + "name": "Free", "description": "Connect PostHog to your favorite tools.", "image_url": "https://posthog.com/images/product/product-icons/integrations.svg", "docs_url": "https://posthog.com/docs/apps", @@ -2668,12 +2717,14 @@ ], "tiers": null, "current_plan": true, - "included_if": "no_active_subscription" + "included_if": "no_active_subscription", + "contact_support": null, + "unit_amount_usd": null }, { "plan_key": "paid-20230117", "product_key": "integrations", - "name": "Integrations", + "name": "Paid", "description": "Connect PostHog to your favorite tools.", "image_url": "https://posthog.com/images/product/product-icons/integrations.svg", "docs_url": "https://posthog.com/docs/apps", @@ -2732,7 +2783,9 @@ ], "tiers": null, "current_plan": false, - "included_if": "has_subscription" + "included_if": "has_subscription", + "contact_support": null, + "unit_amount_usd": null } ], "type": "integrations", @@ -2818,7 +2871,7 @@ { "plan_key": "free-20230117", "product_key": "platform_and_support", - "name": "Platform and support", + "name": "Totally free", "description": "SSO, permission management, and support.", "image_url": "https://posthog.com/images/product/product-icons/platform.svg", "docs_url": "https://posthog.com/docs", @@ -2875,22 +2928,114 @@ "note": null }, { - "key": "terms_and_conditions", - "name": "Terms and conditions", - "description": "Terms and conditions", + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", "unit": null, "limit": null, - "note": "Standard" + "note": null } ], "tiers": null, "current_plan": true, - "included_if": "no_active_subscription" + "included_if": "no_active_subscription", + "contact_support": null, + "unit_amount_usd": null + }, + { + "plan_key": "paid-20240208", + "product_key": "platform_and_support", + "name": "With subscription", + "description": "SSO, permission management, and support.", + "image_url": "https://posthog.com/images/product/product-icons/platform.svg", + "docs_url": "https://posthog.com/docs", + "note": null, + "unit": null, + "free_allocation": null, + "features": [ + { + "key": "tracked_users", + "name": "Tracked users", + "description": "Track users across devices and sessions.", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "team_members", + "name": "Team members", + "description": "PostHog doesn't charge per seat add your entire team!", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "organizations_projects", + "name": "Projects", + "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", + "unit": "projects", + "limit": 2, + "note": null + }, + { + "key": "api_access", + "name": "API access", + "description": "Access your data via our developer-friendly API.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "social_sso", + "name": "SSO via Google, Github, or Gitlab", + "description": "Log in to PostHog with your Google, Github, or Gitlab account.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "community_support", + "name": "Community support", + "description": "Get help from other users and PostHog team members in our Community forums.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "dedicated_support", + "name": "Dedicated account manager", + "description": "Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.", + "unit": null, + "limit": null, + "note": "$2k+/month spend" + }, + { + "key": "email_support", + "name": "Email support", + "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", + "unit": null, + "limit": null, + "note": null + } + ], + "tiers": null, + "current_plan": false, + "included_if": "has_subscription", + "contact_support": null, + "unit_amount_usd": null }, { - "plan_key": "paid-20230926", + "plan_key": "teams-20240208", "product_key": "platform_and_support", - "name": "Platform and support", + "name": "Teams", "description": "SSO, permission management, and support.", "image_url": "https://posthog.com/images/product/product-icons/platform.svg", "docs_url": "https://posthog.com/docs", @@ -2939,17 +3084,25 @@ "note": null }, { - "key": "project_based_permissioning", - "name": "Project permissions", - "description": "Restrict access to data within the organization to only those who need it.", + "key": "sso_enforcement", + "name": "Enforce SSO login", + "description": "Users can only sign up and log in to your PostHog organization with your specified SSO provider.", "unit": null, "limit": null, "note": null }, { - "key": "white_labelling", - "name": "White labeling", - "description": "Use your own branding in your PostHog organization.", + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "2fa_enforcement", + "name": "Enforce 2FA", + "description": "Require all users in your organization to enable two-factor authentication.", "unit": null, "limit": null, "note": null @@ -2962,42 +3115,334 @@ "limit": null, "note": null }, + { + "key": "email_support", + "name": "Email support", + "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", + "unit": null, + "limit": null, + "note": null + }, { "key": "dedicated_support", - "name": "Slack (dedicated channel)", - "description": "Get help directly from our support team in a dedicated Slack channel shared between you and the PostHog team.", + "name": "Dedicated account manager", + "description": "Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.", "unit": null, "limit": null, - "note": "$2k/month spend or above" + "note": "$2k+/month spend" }, { - "key": "email_support", - "name": "Direct access to engineers", - "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", + "key": "priority_support", + "name": "Priority support", + "description": "Get help from our team faster than other customers.", "unit": null, "limit": null, "note": null }, { - "key": "terms_and_conditions", - "name": "Terms and conditions", - "description": "Terms and conditions", + "key": "white_labelling", + "name": "White labeling", + "description": "Use your own branding on surveys, shared dashboards, shared insights, and more.", "unit": null, "limit": null, - "note": "Standard" + "note": null }, { - "key": "security_assessment", - "name": "Security assessment", - "description": "Security assessment", + "key": "project_based_permissioning", + "name": "Project permissions", + "description": "Restrict access to data within the organization to only those who need it.", "unit": null, "limit": null, "note": null - } - ], + }, + { + "key": "advanced_permissions", + "name": "Advanced permissions", + "description": "Control who can access and modify data and features within your organization.", + "unit": null, + "limit": null, + "note": "Project-based only" + }, + { + "key": "audit_logs", + "name": "Audit logs", + "description": "See who in your organization has accessed or modified entities within PostHog.", + "unit": null, + "limit": null, + "note": "Basic" + }, + { + "key": "security_assessment", + "name": "Security assessment", + "description": "Security assessment", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "hipaa_baa", + "name": "HIPAA BAA", + "description": "Get a signed HIPAA Business Associate Agreement (BAA) to use PostHog in a HIPAA-compliant manner.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "team_collaboration", + "name": "Team collaboration features", + "description": "Work together better with tags on dashboards and insights; descriptions on insights, events, & properties; verified events; comments on almost anything.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "ingestion_taxonomy", + "name": "Ingestion taxonomy", + "description": "Mark events as verified or unverified to help you understand the quality of your data.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "tagging", + "name": "Dashboard tags", + "description": "Organize dashboards with tags.", + "unit": null, + "limit": null, + "note": null + } + ], + "tiers": [], + "current_plan": false, + "included_if": null, + "contact_support": null, + "unit_amount_usd": "450.00" + }, + { + "plan_key": "enterprise-20240208", + "product_key": "platform_and_support", + "name": "Enterprise", + "description": "SSO, permission management, and support.", + "image_url": "https://posthog.com/images/product/product-icons/platform.svg", + "docs_url": "https://posthog.com/docs", + "note": null, + "unit": null, + "free_allocation": null, + "features": [ + { + "key": "team_members", + "name": "Team members", + "description": "PostHog doesn't charge per seat add your entire team!", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "organizations_projects", + "name": "Projects", + "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "tracked_users", + "name": "Tracked users", + "description": "Track users across devices and sessions.", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "api_access", + "name": "API access", + "description": "Access your data via our developer-friendly API.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "white_labelling", + "name": "White labeling", + "description": "Use your own branding on surveys, shared dashboards, shared insights, and more.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "team_collaboration", + "name": "Team collaboration features", + "description": "Work together better with tags on dashboards and insights; descriptions on insights, events, & properties; verified events; comments on almost anything.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "ingestion_taxonomy", + "name": "Ingestion taxonomy", + "description": "Mark events as verified or unverified to help you understand the quality of your data.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "tagging", + "name": "Dashboard tags", + "description": "Organize dashboards with tags.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "social_sso", + "name": "SSO via Google, Github, or Gitlab", + "description": "Log in to PostHog with your Google, Github, or Gitlab account.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "sso_enforcement", + "name": "Enforce SSO login", + "description": "Users can only sign up and log in to your PostHog organization with your specified SSO provider.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "saml", + "name": "SAML SSO", + "description": "Allow your organization's users to log in with SAML.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "2fa_enforcement", + "name": "Enforce 2FA", + "description": "Require all users in your organization to enable two-factor authentication.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "project_based_permissioning", + "name": "Project permissions", + "description": "Restrict access to data within the organization to only those who need it.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "role_based_access", + "name": "Role-based access", + "description": "Control access to features like experiments, session recordings, and feature flags with custom roles.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "advanced_permissions", + "name": "Advanced permissions", + "description": "Control who can access and modify data and features within your organization.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "audit_logs", + "name": "Audit logs", + "description": "See who in your organization has accessed or modified entities within PostHog.", + "unit": null, + "limit": null, + "note": "Advanced" + }, + { + "key": "hipaa_baa", + "name": "HIPAA BAA", + "description": "Get a signed HIPAA Business Associate Agreement (BAA) to use PostHog in a HIPAA-compliant manner.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "custom_msa", + "name": "Custom MSA", + "description": "Get a custom Master Services Agreement (MSA) to use PostHog in a way that fits your company's needs.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "community_support", + "name": "Community support", + "description": "Get help from other users and PostHog team members in our Community forums.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "email_support", + "name": "Email support", + "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "dedicated_support", + "name": "Dedicated account manager", + "description": "Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "priority_support", + "name": "Priority support", + "description": "Get help from our team faster than other customers.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "security_assessment", + "name": "Security assessment", + "description": "Security assessment", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "training", + "name": "Ongoing training", + "description": "Get training from our team to help you quickly get up and running with PostHog.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "configuration_support", + "name": "Personalized onboarding", + "description": "Get help from our team to create dashboards that will help you understand your data and your business.", + "unit": null, + "limit": null, + "note": null + } + ], "tiers": null, "current_plan": false, - "included_if": "has_subscription" + "included_if": null, + "contact_support": true, + "unit_amount_usd": null } ], "type": "platform_and_support", @@ -3015,7 +3460,7 @@ "projected_amount_usd": null, "unit": null, "addons": [], - "contact_support": true, + "contact_support": false, "inclusion_only": true, "features": [ { @@ -3051,17 +3496,17 @@ "type": null }, { - "key": "role_based_access", - "name": "Role-based access", - "description": "Control access to features like experiments, session recordings, and feature flags with custom roles.", + "key": "social_sso", + "name": "SSO via Google, Github, or Gitlab", + "description": "Log in to PostHog with your Google, Github, or Gitlab account.", "images": null, "icon_key": null, "type": null }, { - "key": "social_sso", - "name": "SSO via Google, Github, or Gitlab", - "description": "Log in to PostHog with your Google, Github, or Gitlab account.", + "key": "role_based_access", + "name": "Role-based access", + "description": "Control access to features like experiments, session recordings, and feature flags with custom roles.", "images": null, "icon_key": null, "type": null @@ -3074,6 +3519,14 @@ "icon_key": null, "type": null }, + { + "key": "advanced_permissions", + "name": "Advanced permissions", + "description": "Control who can access and modify data and features within your organization.", + "images": null, + "icon_key": null, + "type": null + }, { "key": "saml", "name": "SAML SSO", @@ -3090,10 +3543,26 @@ "icon_key": null, "type": null }, + { + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "2fa_enforcement", + "name": "Enforce 2FA", + "description": "Require all users in your organization to enable two-factor authentication.", + "images": null, + "icon_key": null, + "type": null + }, { "key": "white_labelling", "name": "White labeling", - "description": "Use your own branding in your PostHog organization.", + "description": "Use your own branding on surveys, shared dashboards, shared insights, and more.", "images": null, "icon_key": null, "type": null @@ -3108,31 +3577,31 @@ }, { "key": "dedicated_support", - "name": "Slack (dedicated channel)", - "description": "Get help directly from our support team in a dedicated Slack channel shared between you and the PostHog team.", + "name": "Dedicated account manager", + "description": "Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.", "images": null, "icon_key": null, "type": null }, { "key": "email_support", - "name": "Direct access to engineers", + "name": "Email support", "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", "images": null, "icon_key": null, "type": null }, { - "key": "account_manager", - "name": "Account manager", - "description": "Work with a dedicated account manager to help you get the most out of PostHog.", + "key": "priority_support", + "name": "Priority support", + "description": "Get help from our team faster than other customers.", "images": null, "icon_key": null, "type": null }, { "key": "training", - "name": "Training sessions", + "name": "Ongoing training", "description": "Get training from our team to help you quickly get up and running with PostHog.", "images": null, "icon_key": null, @@ -3140,7 +3609,7 @@ }, { "key": "configuration_support", - "name": "Dashboard configuration support", + "name": "Personalized onboarding", "description": "Get help from our team to create dashboards that will help you understand your data and your business.", "images": null, "icon_key": null, @@ -3185,6 +3654,54 @@ "images": null, "icon_key": null, "type": null + }, + { + "key": "audit_logs", + "name": "Audit logs", + "description": "See who in your organization has accessed or modified entities within PostHog.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "hipaa_baa", + "name": "HIPAA BAA", + "description": "Get a signed HIPAA Business Associate Agreement (BAA) to use PostHog in a HIPAA-compliant manner.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "custom_msa", + "name": "Custom MSA", + "description": "Get a custom Master Services Agreement (MSA) to use PostHog in a way that fits your company's needs.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "team_collaboration", + "name": "Team collaboration features", + "description": "Work together better with tags on dashboards and insights; descriptions on insights, events, & properties; verified events; comments on almost anything.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "ingestion_taxonomy", + "name": "Ingestion taxonomy", + "description": "Mark events as verified or unverified to help you understand the quality of your data.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "tagging", + "name": "Dashboard tags", + "description": "Organize dashboards with tags.", + "images": null, + "icon_key": null, + "type": null } ] } @@ -3213,5 +3730,11 @@ "discount_amount_usd": null, "amount_off_expires_at": null, "never_drop_data": null, - "stripe_portal_url": null + "customer_trust_scores": { + "surveys": 0, + "feature_flags": 0, + "session_replay": 3, + "product_analytics": 3 + }, + "stripe_portal_url": "https://billing.stripe.com/p/session/test_YWNjdF8xSElNRERFdUlhdFJYU2R6LF9QaEVaQ0hCTUE0aE8wUFhlVWVqd29MaElGd3lwRjFa010044U4IxJp" } diff --git a/cypress/fixtures/api/billing-v2/billing-v2.json b/cypress/fixtures/api/billing-v2/billing-v2.json index 605cbf2da1d86..4a8abf3ae41b5 100644 --- a/cypress/fixtures/api/billing-v2/billing-v2.json +++ b/cypress/fixtures/api/billing-v2/billing-v2.json @@ -7,6 +7,7 @@ "surveys_api_mode", "surveys_results_analysis", "surveys_templates", + "surveys_data_retention", "zapier", "slack_integration", "microsoft_teams_integration", @@ -14,6 +15,7 @@ "apps", "app_metrics", "boolean_flags", + "multivariate_flags", "persist_flags_cross_authentication", "feature_flag_payloads", "multiple_release_conditions", @@ -21,46 +23,53 @@ "targeting_by_group", "local_evaluation_and_bootstrapping", "flag_usage_stats", - "data_warehouse_manual_sync", - "data_warehouse_unified_querying", - "data_warehouse_insights_visualization", + "experimentation", + "funnel_experiments", + "secondary_metrics", + "statistical_analysis", + "feature_flags_data_retention", "console_logs", + "recordings_performance", + "session_replay_network_payloads", "recordings_playlists", + "session_replay_data_retention", + "replay_mask_sensitive_data", + "replay_sharing_embedding", + "replay_product_analytics_integration", + "replay_filter_person_properties", + "replay_filter_events", + "replay_dom_explorer", + "session_replay_sampling", + "replay_recording_duration_minimum", + "replay_feature_flag_based_recording", "dashboards", "funnels", "graphs_trends", "paths", "subscriptions", "paths_advanced", - "advanced_permissions", - "team_collaboration", - "ingestion_taxonomy", "correlation_analysis", - "tagging", "behavioral_cohort_filtering", + "product_analytics_data_retention", "tracked_users", - "data_retention", "team_members", "organizations_projects", "api_access", "social_sso", - "project_based_permissioning", - "white_labelling", "community_support", "dedicated_support", "email_support", - "terms_and_conditions", - "security_assessment" + "2fa" ], "license": { "plan": "dev" }, - "customer_id": "cus_Ot0pdGiqNz8M9J", + "customer_id": "cus_Pg7PIL8MsKi6bx", "deactivated": false, "has_active_subscription": true, "billing_period": { - "current_period_start": "2023-10-31T00:08:23Z", - "current_period_end": "2023-11-30T00:08:23Z", + "current_period_start": "2024-03-07T23:21:20Z", + "current_period_end": "2024-04-07T23:21:20Z", "interval": "month" }, "available_product_features": [ @@ -82,8 +91,8 @@ }, { "key": "surveys_user_targeting", - "name": "User property targeting", - "description": "Target users based on any of their user properties.", + "name": "Advanced user targeting", + "description": "Target by URL, user property, or feature flag when used with Feature flags.", "unit": null, "limit": null, "note": null @@ -99,15 +108,15 @@ { "key": "surveys_api_mode", "name": "API mode", - "description": "Create surveys via the API.", + "description": "Using PostHog.js? No more code required. But if want to create your own UI, we have a full API.", "unit": null, "limit": null, "note": null }, { "key": "surveys_results_analysis", - "name": "Results analysis", - "description": "Analyze your survey results including completion rates and drop offs.", + "name": "Aggregated results", + "description": "See feedback summarized and broken down per response, plus completion rates and drop offs.", "unit": null, "limit": null, "note": null @@ -120,6 +129,14 @@ "limit": null, "note": null }, + { + "key": "surveys_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "year", + "limit": 1, + "note": null + }, { "key": "zapier", "name": "Zapier", @@ -154,8 +171,8 @@ }, { "key": "apps", - "name": "CDP + Apps library", - "description": "Connect your data with 50+ apps including BigQuery, Redshift, and more.", + "name": "Apps", + "description": "Use apps to transform, filter, and modify your incoming data. (Export apps not included, see the Data pipelines addon for product analytics.)", "unit": null, "limit": null, "note": null @@ -176,6 +193,14 @@ "limit": null, "note": null }, + { + "key": "multivariate_flags", + "name": "Multivariate feature flags & experiments", + "description": "Create three or more variants of a feature flag to test or release different versions of a feature.", + "unit": null, + "limit": null, + "note": null + }, { "key": "persist_flags_cross_authentication", "name": "Persist flags across authentication", @@ -186,8 +211,8 @@ }, { "key": "feature_flag_payloads", - "name": "Payloads", - "description": "Send additional pieces of information (any valid JSON) to your app when a flag is matched for a user.", + "name": "Test changes without code", + "description": "Use JSON payloads to change text, visuals, or entire blocks of code without subsequent deployments.", "unit": null, "limit": null, "note": null @@ -195,7 +220,7 @@ { "key": "multiple_release_conditions", "name": "Multiple release conditions", - "description": "Target multiple groups of users with different release conditions for the same feature flag.", + "description": "Customize your rollout strategy by user or group properties, cohort, or trafic percentage.", "unit": null, "limit": null, "note": null @@ -233,33 +258,65 @@ "note": null }, { - "key": "data_warehouse_manual_sync", - "name": "Manual sync", - "description": "Sync your data to the warehouse using your cloud storage provider.", + "key": "experimentation", + "name": "A/B testing", + "description": "Test changes to your product and evaluate the impacts those changes make.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "funnel_experiments", + "name": "Funnel & trend experiments", + "description": "Measure the impact of a change on a aggregate values or a series of events, like a signup flow.", "unit": null, "limit": null, "note": null }, { - "key": "data_warehouse_unified_querying", - "name": "Unified querying", - "description": "Query all your business and product data directly inside PostHog.", + "key": "secondary_metrics", + "name": "Secondary experiment metrics", + "description": "Track additional metrics to see how your experiment affects other parts of your app or different flows.", "unit": null, "limit": null, "note": null }, { - "key": "data_warehouse_insights_visualization", - "name": "Insights", - "description": "Create insights from the data you import and add them to your PostHog dashboards.", + "key": "statistical_analysis", + "name": "Statistical analysis", + "description": "Get a statistical analysis of your experiment results to see if the results are significant, or if they're likely just due to chance.", "unit": null, "limit": null, "note": null }, + { + "key": "feature_flags_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "year", + "limit": 1, + "note": null + }, { "key": "console_logs", "name": "Console logs", - "description": "Diagnose issues by inspecting errors in the user's network console", + "description": "Debug issues faster by browsing the user's console.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "recordings_performance", + "name": "Network performance on recordings", + "description": "See your end-user's network performance and information alongside session recordings.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "session_replay_network_payloads", + "name": "Network monitor", + "description": "Analyze performance and network calls.", "unit": null, "limit": null, "note": null @@ -273,116 +330,164 @@ "note": null }, { - "key": "dashboards", - "name": "Dashboards", - "description": "Save trends, funnels, and other insights for easy reference by your whole team.", + "key": "session_replay_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "month", + "limit": 1, + "note": null + }, + { + "key": "replay_mask_sensitive_data", + "name": "Block sensitive data", + "description": "Disable capturing data from any DOM element with HTML attributes or a customizable config.", "unit": null, "limit": null, "note": null }, { - "key": "funnels", - "name": "Funnels", - "description": "Visualize user dropoff between a sequence of events.", + "key": "replay_sharing_embedding", + "name": "Share and embed", + "description": "Share replays directly via URL or embed via iframe.", "unit": null, "limit": null, "note": null }, { - "key": "graphs_trends", - "name": "Graphs & trends", - "description": "Plot any number of events or actions over time.", + "key": "replay_product_analytics_integration", + "name": "Event timeline", + "description": "See a history of everything that happened in a user's session.", "unit": null, "limit": null, "note": null }, { - "key": "paths", - "name": "Paths", - "description": "Limited paths excludes: customizing path insights by setting the maximum number of paths, number of people on each path, how path names appear", + "key": "replay_filter_person_properties", + "name": "Filter person properties", + "description": "Filter by person properties to quickly find relevant recordings.", "unit": null, "limit": null, "note": null }, { - "key": "subscriptions", - "name": "Insight & dashboard subscriptions", - "description": "Create a subscription for any insight or dashboard in PostHog to receive regular reports with their updates.", + "key": "replay_filter_events", + "name": "Filter events", + "description": "Filter by events to quickly find relevant recordings.", "unit": null, "limit": null, "note": null }, { - "key": "paths_advanced", - "name": "Advanced paths", - "description": "Customize your path insights by setting the maximum number of paths, number of people on each path, and how path names should appear.", + "key": "replay_dom_explorer", + "name": "DOM Explorer", + "description": "Freeze snapshots of recordings and explore the DOM with your browser dev tools.", "unit": null, "limit": null, "note": null }, { - "key": "advanced_permissions", - "name": "Dashboard permissions", - "description": "Restrict access to dashboards within the organization to only those who need it.", + "key": "session_replay_sampling", + "name": "Sample recorded sessions", + "description": "Restrict the percentage of sessions that will be recorded.", "unit": null, "limit": null, "note": null }, { - "key": "team_collaboration", - "name": "Tags & text cards", - "description": "Keep organized by adding tags to your dashboards, cohorts and more. Add text cards and descriptions to your dashboards to provide context to your team.", + "key": "replay_recording_duration_minimum", + "name": "Minimum duration", + "description": "Only record sessions longer than the minimum duration.", "unit": null, "limit": null, "note": null }, { - "key": "ingestion_taxonomy", - "name": "Ingestion taxonomy", - "description": "Ingestion taxonomy", + "key": "replay_feature_flag_based_recording", + "name": "Record via feature flag", + "description": "Only record sessions for users that have the flag enabled.", "unit": null, "limit": null, "note": null }, { - "key": "correlation_analysis", - "name": "Correlation analysis", - "description": "Automatically highlight significant factors that affect the conversion rate of users within a funnel.", + "key": "dashboards", + "name": "Dashboards", + "description": "Save trends, funnels, and other insights for easy reference by your whole team.", "unit": null, "limit": null, "note": null }, { - "key": "tagging", - "name": "Dashboard tags", - "description": "Organize dashboards with tags.", + "key": "funnels", + "name": "Funnels", + "description": "Visualize user dropoff between a sequence of events. See conversion rate over time, use flexible step ordering, set exclusion steps, and more.", "unit": null, "limit": null, "note": null }, { - "key": "behavioral_cohort_filtering", - "name": "Lifecycle cohorts", - "description": "Group users based on their long term behavior, such as whether they frequently performed an event, or have recently stopped performing an event.", + "key": "graphs_trends", + "name": "Graphs & trends", + "description": "Plot any number of events or actions over time.", "unit": null, "limit": null, "note": null }, { - "key": "tracked_users", - "name": "Tracked users", - "description": "Track users across devices and sessions.", + "key": "paths", + "name": "User paths", + "description": "Limited paths excludes: customizing path insights by setting the maximum number of paths, number of people on each path, how path names appear", "unit": null, "limit": null, - "note": "Unlimited" + "note": null + }, + { + "key": "subscriptions", + "name": "Insight & dashboard subscriptions", + "description": "Create a subscription for any insight or dashboard in PostHog to receive regular reports with their updates.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "paths_advanced", + "name": "Advanced paths", + "description": "Customize your path insights by setting the maximum number of paths, number of people on each path, and how path names should appear.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "correlation_analysis", + "name": "Correlation analysis", + "description": "Automatically highlight significant factors that affect the conversion rate of users within a funnel.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "behavioral_cohort_filtering", + "name": "Lifecycle", + "description": "Discover how your active users break down, highlighting those who have recently stopped being active or those who have just become active for the first time.", + "unit": null, + "limit": null, + "note": null }, { - "key": "data_retention", + "key": "product_analytics_data_retention", "name": "Data retention", "description": "Keep a historical record of your data.", + "unit": "years", + "limit": 7, + "note": null + }, + { + "key": "tracked_users", + "name": "Tracked users", + "description": "Track users across devices and sessions.", "unit": null, "limit": null, - "note": "7 years" + "note": "Unlimited" }, { "key": "team_members", @@ -396,9 +501,9 @@ "key": "organizations_projects", "name": "Projects", "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", - "unit": null, - "limit": null, - "note": "Unlimited" + "unit": "projects", + "limit": 2, + "note": null }, { "key": "api_access", @@ -416,22 +521,6 @@ "limit": null, "note": null }, - { - "key": "project_based_permissioning", - "name": "Project permissions", - "description": "Restrict access to data within the organization to only those who need it.", - "unit": null, - "limit": null, - "note": null - }, - { - "key": "white_labelling", - "name": "White labeling", - "description": "Use your own branding in your PostHog organization.", - "unit": null, - "limit": null, - "note": null - }, { "key": "community_support", "name": "Community support", @@ -442,32 +531,24 @@ }, { "key": "dedicated_support", - "name": "Slack (dedicated channel)", - "description": "Get help directly from our support team in a dedicated Slack channel shared between you and the PostHog team.", + "name": "Dedicated account manager", + "description": "Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.", "unit": null, "limit": null, - "note": "$2k/month spend or above" + "note": "$2k+/month spend" }, { "key": "email_support", - "name": "Direct access to engineers", + "name": "Email support", "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", "unit": null, "limit": null, "note": null }, { - "key": "terms_and_conditions", - "name": "Terms and conditions", - "description": "Terms and conditions", - "unit": null, - "limit": null, - "note": "Standard" - }, - { - "key": "security_assessment", - "name": "Security assessment", - "description": "Security assessment", + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", "unit": null, "limit": null, "note": null @@ -477,11 +558,13 @@ "current_total_amount_usd_after_discount": "0.00", "products": [ { - "name": "Product analytics + data stack", - "description": "Trends, funnels, path analysis, CDP + more.", + "name": "Product analytics", + "headline": "Product analytics with autocapture", + "description": "A comprehensive product analytics platform built to natively work with session replay, feature flags, A/B testing, and surveys.", "price_description": null, "usage_key": "events", - "image_url": "https://posthog.com/images/product/product-icons/product-analytics.svg", + "image_url": "https://posthog.com/images/products/product-analytics/product-analytics.png", + "screenshot_url": "https://posthog.com/images/products/product-analytics/screenshot-product-analytics.png", "icon_key": "IconGraph", "docs_url": "https://posthog.com/docs/product-analytics", "subscribed": true, @@ -489,9 +572,9 @@ { "plan_key": "free-20230117", "product_key": "product_analytics", - "name": "Product analytics + data stack", - "description": "Trends, funnels, path analysis, CDP + more.", - "image_url": "https://posthog.com/images/product/product-icons/product-analytics.svg", + "name": "Free", + "description": "A comprehensive product analytics platform built to natively work with session replay, feature flags, A/B testing, and surveys.", + "image_url": "https://posthog.com/images/products/product-analytics/product-analytics.png", "docs_url": "https://posthog.com/docs/product-analytics", "note": null, "unit": "event", @@ -508,7 +591,7 @@ { "key": "funnels", "name": "Funnels", - "description": "Visualize user dropoff between a sequence of events.", + "description": "Visualize user dropoff between a sequence of events. See conversion rate over time, use flexible step ordering, set exclusion steps, and more.", "unit": null, "limit": null, "note": null @@ -523,23 +606,33 @@ }, { "key": "paths", - "name": "Paths", + "name": "User paths", "description": "Limited paths excludes: customizing path insights by setting the maximum number of paths, number of people on each path, how path names appear", "unit": null, "limit": null, "note": "Limited" + }, + { + "key": "product_analytics_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "year", + "limit": 1, + "note": null } ], "tiers": null, "current_plan": false, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null }, { - "plan_key": "paid-20230509", + "plan_key": "paid-20240111", "product_key": "product_analytics", - "name": "Product analytics + data stack", - "description": "Trends, funnels, path analysis, CDP + more.", - "image_url": "https://posthog.com/images/product/product-icons/product-analytics.svg", + "name": "Paid", + "description": "A comprehensive product analytics platform built to natively work with session replay, feature flags, A/B testing, and surveys.", + "image_url": "https://posthog.com/images/products/product-analytics/product-analytics.png", "docs_url": "https://posthog.com/docs/product-analytics", "note": null, "unit": "event", @@ -556,7 +649,7 @@ { "key": "funnels", "name": "Funnels", - "description": "Visualize user dropoff between a sequence of events.", + "description": "Visualize user dropoff between a sequence of events. See conversion rate over time, use flexible step ordering, set exclusion steps, and more.", "unit": null, "limit": null, "note": null @@ -571,7 +664,7 @@ }, { "key": "paths", - "name": "Paths", + "name": "User paths", "description": "Limited paths excludes: customizing path insights by setting the maximum number of paths, number of people on each path, how path names appear", "unit": null, "limit": null, @@ -593,30 +686,6 @@ "limit": null, "note": null }, - { - "key": "advanced_permissions", - "name": "Dashboard permissions", - "description": "Restrict access to dashboards within the organization to only those who need it.", - "unit": null, - "limit": null, - "note": null - }, - { - "key": "team_collaboration", - "name": "Tags & text cards", - "description": "Keep organized by adding tags to your dashboards, cohorts and more. Add text cards and descriptions to your dashboards to provide context to your team.", - "unit": null, - "limit": null, - "note": null - }, - { - "key": "ingestion_taxonomy", - "name": "Ingestion taxonomy", - "description": "Ingestion taxonomy", - "unit": null, - "limit": null, - "note": null - }, { "key": "correlation_analysis", "name": "Correlation analysis", @@ -626,19 +695,19 @@ "note": null }, { - "key": "tagging", - "name": "Dashboard tags", - "description": "Organize dashboards with tags.", + "key": "behavioral_cohort_filtering", + "name": "Lifecycle", + "description": "Discover how your active users break down, highlighting those who have recently stopped being active or those who have just become active for the first time.", "unit": null, "limit": null, "note": null }, { - "key": "behavioral_cohort_filtering", - "name": "Lifecycle cohorts", - "description": "Group users based on their long term behavior, such as whether they frequently performed an event, or have recently stopped performing an event.", - "unit": null, - "limit": null, + "key": "product_analytics_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "years", + "limit": 7, "note": null } ], @@ -654,7 +723,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0003068", + "unit_amount_usd": "0.00031", "up_to": 2000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -708,7 +777,9 @@ } ], "current_plan": true, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], "type": "product_analytics", @@ -725,7 +796,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0003068", + "unit_amount_usd": "0.00031", "up_to": 2000000, "current_amount_usd": "0.00", "current_usage": 0, @@ -782,7 +853,7 @@ "unit_amount_usd": null, "current_amount_usd_before_addons": "0.00", "current_amount_usd": "0.00", - "current_usage": 35, + "current_usage": 0, "usage_limit": null, "has_exceeded_limit": false, "percentage_usage": 0, @@ -876,7 +947,7 @@ { "plan_key": "addon-20230509", "product_key": "group_analytics", - "name": "Group analytics", + "name": "Addon", "description": "Associate events with a group or entity - such as a company, community, or project. Analyze these events as if they were sent by that entity itself. Great for B2B, marketplaces, and more.", "image_url": "https://posthog.com/images/product/product-icons/group-analytics.svg", "docs_url": "https://posthog.com/docs/product-analytics/group-analytics", @@ -959,106 +1030,44 @@ } ], "current_plan": false, - "included_if": null - } - ], - "contact_support": false - } - ], - "contact_support": false, - "inclusion_only": false - }, - { - "name": "Session replay", - "description": "Searchable recordings of people using your app or website with console logs and behavioral bucketing.", - "price_description": null, - "usage_key": "recordings", - "image_url": "https://posthog.com/images/product/product-icons/session-replay.svg", - "icon_key": "IconRewindPlay", - "docs_url": "https://posthog.com/docs/session-replay", - "subscribed": true, - "plans": [ - { - "plan_key": "free-20230117", - "product_key": "session_replay", - "name": "Session replay", - "description": "Searchable recordings of people using your app or website with console logs and behavioral bucketing.", - "image_url": "https://posthog.com/images/product/product-icons/session-replay.svg", - "docs_url": "https://posthog.com/docs/session-replay", - "note": null, - "unit": "recording", - "free_allocation": 15000, - "features": [ - { - "key": "console_logs", - "name": "Console logs", - "description": "Diagnose issues by inspecting errors in the user's network console", - "unit": null, - "limit": null, - "note": null - }, - { - "key": "recordings_playlists", - "name": "Recording playlists", - "description": "Create playlists of certain session recordings to easily find and watch them again in the future.", - "unit": "playlists", - "limit": 5, - "note": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], - "tiers": null, - "current_plan": true, - "included_if": null + "contact_support": null }, { - "plan_key": "paid-20230117", - "product_key": "session_replay", - "name": "Session replay", - "description": "Searchable recordings of people using your app or website with console logs and behavioral bucketing.", - "image_url": "https://posthog.com/images/product/product-icons/session-replay.svg", - "docs_url": "https://posthog.com/docs/session-replay", - "note": null, - "unit": "recording", - "free_allocation": null, - "features": [ - { - "key": "console_logs", - "name": "Console logs", - "description": "Diagnose issues by inspecting errors in the user's network console", - "unit": null, - "limit": null, - "note": null - }, + "name": "Data pipelines", + "description": "Get your PostHog data into your data warehouse or other tools like BigQuery, Redshift, Customer.io, and more.", + "price_description": null, + "image_url": "None", + "icon_key": "IconDecisionTree", + "docs_url": "https://posthog.com/docs/cdp/batch-exports", + "type": "data_pipelines", + "tiers": [ { - "key": "recordings_playlists", - "name": "Recording playlists", - "description": "Create playlists of certain session recordings to easily find and watch them again in the future.", - "unit": null, - "limit": null, - "note": null + "flat_amount_usd": "0", + "unit_amount_usd": "0", + "up_to": 1000000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null }, { - "key": "recordings_performance", - "name": "Network performance on recordings", - "description": "See your end-user's network performance and information alongside session recordings.", - "unit": null, - "limit": null, - "note": null + "flat_amount_usd": "0", + "unit_amount_usd": "0.000248", + "up_to": 2000000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null }, - { - "key": "recordings_file_export", - "name": "Recordings file export", - "description": "Save session recordings as a file to your local filesystem.", - "unit": null, - "limit": null, - "note": null - } - ], - "tiers": [ { "flat_amount_usd": "0", - "unit_amount_usd": "0", - "up_to": 15000, + "unit_amount_usd": "0.000104", + "up_to": 15000000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1066,8 +1075,8 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.005", - "up_to": 50000, + "unit_amount_usd": "0.0000655", + "up_to": 50000000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1075,8 +1084,8 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0045", - "up_to": 150000, + "unit_amount_usd": "0.0000364", + "up_to": 100000000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1084,8 +1093,8 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.004", - "up_to": 500000, + "unit_amount_usd": "0.0000187", + "up_to": 250000000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1093,7 +1102,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0035", + "unit_amount_usd": "0.0000042", "up_to": null, "current_amount_usd": "0.00", "current_usage": 0, @@ -1101,109 +1110,384 @@ "projected_amount_usd": null } ], - "current_plan": false, - "included_if": null + "tiered": true, + "included_with_main_product": false, + "subscribed": false, + "unit": "event", + "unit_amount_usd": null, + "current_amount_usd": null, + "current_usage": 0, + "projected_usage": 0, + "projected_amount_usd": "0.00", + "plans": [ + { + "plan_key": "addon-20240111", + "product_key": "data_pipelines", + "name": "Addon", + "description": "Get your PostHog data into your data warehouse or other tools like BigQuery, Redshift, Customer.io, and more.", + "image_url": null, + "docs_url": "https://posthog.com/docs/cdp/batch-exports", + "note": null, + "unit": "event", + "free_allocation": null, + "features": [ + { + "key": "data_pipelines", + "name": "Data pipelines", + "description": "Get your PostHog data into your data warehouse or other tools like BigQuery, Redshift, Customer.io, and more.", + "unit": null, + "limit": null, + "note": null + } + ], + "tiers": [ + { + "flat_amount_usd": "0", + "unit_amount_usd": "0", + "up_to": 1000000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.000248", + "up_to": 2000000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.000104", + "up_to": 15000000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.0000655", + "up_to": 50000000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.0000364", + "up_to": 100000000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.0000187", + "up_to": 250000000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.0000042", + "up_to": null, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + } + ], + "current_plan": false, + "included_if": null, + "contact_support": null, + "unit_amount_usd": null + } + ], + "contact_support": null } ], - "type": "session_replay", - "free_allocation": 15000, - "tiers": null, - "tiered": true, - "unit_amount_usd": null, - "current_amount_usd_before_addons": null, - "current_amount_usd": null, - "current_usage": 0, - "usage_limit": 15000, - "has_exceeded_limit": false, - "percentage_usage": 0, - "projected_usage": 0, - "projected_amount_usd": null, - "unit": "recording", - "addons": [], "contact_support": false, - "inclusion_only": false + "inclusion_only": false, + "features": [ + { + "key": "product_analytics_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "dashboards", + "name": "Dashboards", + "description": "Save trends, funnels, and other insights for easy reference by your whole team.", + "images": { + "light": "https://posthog.com/images/products/product-analytics/screenshot-dashboards.png", + "dark": "https://posthog.com/images/products/product-analytics/screenshot-dashboards-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "funnels", + "name": "Funnels", + "description": "Visualize user dropoff between a sequence of events. See conversion rate over time, use flexible step ordering, set exclusion steps, and more.", + "images": { + "light": "https://posthog.com/images/products/product-analytics/screenshot-funnels.png", + "dark": "https://posthog.com/images/products/product-analytics/screenshot-funnels-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "graphs_trends", + "name": "Graphs & trends", + "description": "Plot any number of events or actions over time.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "paths", + "name": "User paths", + "description": "Limited paths excludes: customizing path insights by setting the maximum number of paths, number of people on each path, how path names appear", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "insights", + "name": "Unlimited Insights", + "description": "Trends, funnels, retention, user paths, stickiness, and lifecycle insights to visualize your data.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "subscriptions", + "name": "Insight & dashboard subscriptions", + "description": "Create a subscription for any insight or dashboard in PostHog to receive regular reports with their updates.", + "images": null, + "icon_key": "IconNotification", + "type": "secondary" + }, + { + "key": "paths_advanced", + "name": "Advanced paths", + "description": "Customize your path insights by setting the maximum number of paths, number of people on each path, and how path names should appear.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "correlation_analysis", + "name": "Correlation analysis", + "description": "Automatically highlight significant factors that affect the conversion rate of users within a funnel.", + "images": { + "light": "https://posthog.com/images/products/product-analytics/screenshot-correlation-analysis.png", + "dark": "https://posthog.com/images/products/product-analytics/screenshot-correlation-analysis-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "behavioral_cohort_filtering", + "name": "Lifecycle", + "description": "Discover how your active users break down, highlighting those who have recently stopped being active or those who have just become active for the first time.", + "images": { + "light": "https://posthog.com/images/products/product-analytics/screenshot-lifecycle.png", + "dark": "https://posthog.com/images/products/product-analytics/screenshot-lifecycle-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "product_analytics_retention", + "name": "Retention", + "description": "See how many users return on subsequent days after performing an event the first time, or recurrently.", + "images": { + "light": "https://posthog.com/images/products/product-analytics/screenshot-retention.png", + "dark": "https://posthog.com/images/products/product-analytics/screenshot-retention-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "product_analytics_stickiness", + "name": "Stickiness", + "description": "Learn how many times users perform a specific event in a period of time.", + "images": { + "light": "https://posthog.com/images/products/product-analytics/screenshot-stickiness.png", + "dark": "https://posthog.com/images/products/product-analytics/screenshot-stickiness-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "autocapture", + "name": "Autocapture", + "description": "Add PostHog.js to your website or web app to track all event data and retroactively define events.", + "images": null, + "icon_key": "IconBolt", + "type": "secondary" + }, + { + "key": "data_visualization", + "name": "Data visualization", + "description": "Filter data by user property, group data, and use formulas in queries.", + "images": null, + "icon_key": "IconPieChart", + "type": "secondary" + }, + { + "key": "product_analytics_sql_queries", + "name": "Query with SQL", + "description": "Use PostHog’s filtering interface or switch into SQL mode for more powerful querying.", + "images": null, + "icon_key": "IconTerminal", + "type": "secondary" + } + ] }, { - "name": "Feature flags & A/B testing", - "description": "Safely roll out new features and run experiments on changes.", + "name": "Session replay", + "headline": "Watch how users experience your app", + "description": "Session replay helps you diagnose issues and understand user behavior in your product or website.", "price_description": null, - "usage_key": "feature_flag_requests", - "image_url": "https://posthog.com/images/product/product-icons/feature-flags.svg", - "icon_key": "IconToggle", - "docs_url": "https://posthog.com/docs/feature-flags", - "subscribed": false, + "usage_key": "recordings", + "image_url": "https://posthog.com/images/products/session-replay/session-replay.png", + "screenshot_url": "https://posthog.com/images/products/session-replay/screenshot-session-replay.png", + "icon_key": "IconRewindPlay", + "docs_url": "https://posthog.com/docs/session-replay", + "subscribed": true, "plans": [ { - "plan_key": "free-20230117", - "product_key": "feature_flags", - "name": "Feature flags & A/B testing", - "description": "Safely roll out new features and run experiments on changes.", - "image_url": "https://posthog.com/images/product/product-icons/feature-flags.svg", - "docs_url": "https://posthog.com/docs/feature-flags", + "plan_key": "free-20231218", + "product_key": "session_replay", + "name": "Free", + "description": "Session replay helps you diagnose issues and understand user behavior in your product or website.", + "image_url": "https://posthog.com/images/products/session-replay/session-replay.png", + "docs_url": "https://posthog.com/docs/session-replay", "note": null, - "unit": "request", - "free_allocation": 1000000, + "unit": "recording", + "free_allocation": 5000, "features": [ { - "key": "boolean_flags", - "name": "Boolean feature flags", - "description": "Turn features on and off for specific users.", + "key": "console_logs", + "name": "Console logs", + "description": "Debug issues faster by browsing the user's console.", "unit": null, "limit": null, "note": null }, { - "key": "persist_flags_cross_authentication", - "name": "Persist flags across authentication", - "description": "Persist feature flags across authentication events so that flag values don't change when an anonymous user logs in and becomes identified.", + "key": "recordings_performance", + "name": "Network performance on recordings", + "description": "See your end-user's network performance and information alongside session recordings.", "unit": null, "limit": null, "note": null }, { - "key": "feature_flag_payloads", - "name": "Payloads", - "description": "Send additional pieces of information (any valid JSON) to your app when a flag is matched for a user.", + "key": "session_replay_network_payloads", + "name": "Network monitor", + "description": "Analyze performance and network calls.", "unit": null, "limit": null, "note": null }, { - "key": "multiple_release_conditions", - "name": "Multiple release conditions", - "description": "Target multiple groups of users with different release conditions for the same feature flag.", + "key": "recordings_playlists", + "name": "Recording playlists", + "description": "Create playlists of certain session recordings to easily find and watch them again in the future.", + "unit": "playlists", + "limit": 5, + "note": null + }, + { + "key": "session_replay_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "month", + "limit": 1, + "note": null + }, + { + "key": "replay_mask_sensitive_data", + "name": "Block sensitive data", + "description": "Disable capturing data from any DOM element with HTML attributes or a customizable config.", "unit": null, "limit": null, "note": null }, { - "key": "release_condition_overrides", - "name": "Release condition overrides", - "description": "For any release condition, specify which flag value the users or groups in that condition should receive.", + "key": "replay_sharing_embedding", + "name": "Share and embed", + "description": "Share replays directly via URL or embed via iframe.", "unit": null, "limit": null, "note": null }, { - "key": "targeting_by_group", - "name": "Flag targeting by groups", - "description": "Target feature flag release conditions by group properties, not just user properties.", + "key": "replay_product_analytics_integration", + "name": "Event timeline", + "description": "See a history of everything that happened in a user's session.", "unit": null, "limit": null, "note": null }, { - "key": "local_evaluation_and_bootstrapping", - "name": "Local evaluation & bootstrapping", - "description": "Bootstrap flags on initialization so all flags are available immediately, without having to make extra network requests.", + "key": "replay_filter_person_properties", + "name": "Filter person properties", + "description": "Filter by person properties to quickly find relevant recordings.", "unit": null, "limit": null, "note": null }, { - "key": "flag_usage_stats", - "name": "Flag usage stats", - "description": "See how many times a flag has been evaluated, how many times each variant has been returned, and what values users received.", + "key": "replay_filter_events", + "name": "Filter events", + "description": "Filter by events to quickly find relevant recordings.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "replay_dom_explorer", + "name": "DOM Explorer", + "description": "Freeze snapshots of recordings and explore the DOM with your browser dev tools.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "session_replay_sampling", + "name": "Sample recorded sessions", + "description": "Restrict the percentage of sessions that will be recorded.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "replay_recording_duration_minimum", + "name": "Minimum duration", + "description": "Only record sessions longer than the minimum duration.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "replay_feature_flag_based_recording", + "name": "Record via feature flag", + "description": "Only record sessions for users that have the flag enabled.", "unit": null, "limit": null, "note": null @@ -1211,127 +1495,137 @@ ], "tiers": null, "current_plan": true, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null }, { - "plan_key": "paid-20230623", - "product_key": "feature_flags", - "name": "Feature flags & A/B testing", - "description": "Safely roll out new features and run experiments on changes.", - "image_url": "https://posthog.com/images/product/product-icons/feature-flags.svg", - "docs_url": "https://posthog.com/docs/feature-flags", + "plan_key": "paid-20231218", + "product_key": "session_replay", + "name": "Paid", + "description": "Session replay helps you diagnose issues and understand user behavior in your product or website.", + "image_url": "https://posthog.com/images/products/session-replay/session-replay.png", + "docs_url": "https://posthog.com/docs/session-replay", "note": null, - "unit": "request", + "unit": "recording", "free_allocation": null, "features": [ { - "key": "boolean_flags", - "name": "Boolean feature flags", - "description": "Turn features on and off for specific users.", + "key": "console_logs", + "name": "Console logs", + "description": "Debug issues faster by browsing the user's console.", "unit": null, "limit": null, "note": null }, { - "key": "multivariate_flags", - "name": "Multivariate feature flags & experiments", - "description": "Create three or more variants of a feature flag to test or release different versions of a feature.", + "key": "recordings_playlists", + "name": "Recording playlists", + "description": "Create playlists of certain session recordings to easily find and watch them again in the future.", "unit": null, "limit": null, "note": null }, { - "key": "persist_flags_cross_authentication", - "name": "Persist flags across authentication", - "description": "Persist feature flags across authentication events so that flag values don't change when an anonymous user logs in and becomes identified.", + "key": "recordings_performance", + "name": "Network performance on recordings", + "description": "See your end-user's network performance and information alongside session recordings.", "unit": null, "limit": null, "note": null }, { - "key": "feature_flag_payloads", - "name": "Payloads", - "description": "Send additional pieces of information (any valid JSON) to your app when a flag is matched for a user.", + "key": "session_replay_network_payloads", + "name": "Network monitor", + "description": "Analyze performance and network calls.", "unit": null, "limit": null, "note": null }, { - "key": "multiple_release_conditions", - "name": "Multiple release conditions", - "description": "Target multiple groups of users with different release conditions for the same feature flag.", + "key": "recordings_file_export", + "name": "Download recordings", + "description": "Retain recordings beyond data retention limits.", "unit": null, "limit": null, "note": null }, { - "key": "release_condition_overrides", - "name": "Release condition overrides", - "description": "For any release condition, specify which flag value the users or groups in that condition should receive.", + "key": "session_replay_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "months", + "limit": 3, + "note": null + }, + { + "key": "replay_mask_sensitive_data", + "name": "Block sensitive data", + "description": "Disable capturing data from any DOM element with HTML attributes or a customizable config.", "unit": null, "limit": null, "note": null }, { - "key": "targeting_by_group", - "name": "Flag targeting by groups", - "description": "Target feature flag release conditions by group properties, not just user properties.", + "key": "replay_sharing_embedding", + "name": "Share and embed", + "description": "Share replays directly via URL or embed via iframe.", "unit": null, "limit": null, "note": null }, { - "key": "local_evaluation_and_bootstrapping", - "name": "Local evaluation & bootstrapping", - "description": "Bootstrap flags on initialization so all flags are available immediately, without having to make extra network requests.", + "key": "replay_product_analytics_integration", + "name": "Event timeline", + "description": "See a history of everything that happened in a user's session.", "unit": null, "limit": null, "note": null }, { - "key": "flag_usage_stats", - "name": "Flag usage stats", - "description": "See how many times a flag has been evaluated, how many times each variant has been returned, and what values users received.", + "key": "replay_filter_person_properties", + "name": "Filter person properties", + "description": "Filter by person properties to quickly find relevant recordings.", "unit": null, "limit": null, "note": null }, { - "key": "experimentation", - "name": "A/B testing", - "description": "Test changes to your product and evaluate the impacts those changes make.", + "key": "replay_filter_events", + "name": "Filter events", + "description": "Filter by events to quickly find relevant recordings.", "unit": null, "limit": null, "note": null }, { - "key": "group_experiments", - "name": "Group experiments", - "description": "Target experiments to specific groups of users so everyone in the same group gets the same variant.", + "key": "replay_dom_explorer", + "name": "DOM Explorer", + "description": "Freeze snapshots of recordings and explore the DOM with your browser dev tools.", "unit": null, "limit": null, "note": null }, { - "key": "funnel_experiments", - "name": "Funnel & trend experiments", - "description": "Measure the impact of a change on a aggregate values or a series of events, like a signup flow.", + "key": "session_replay_sampling", + "name": "Sample recorded sessions", + "description": "Restrict the percentage of sessions that will be recorded.", "unit": null, "limit": null, "note": null }, { - "key": "secondary_metrics", - "name": "Secondary experiment metrics", - "description": "Track additional metrics to see how your experiment affects other parts of your app or different flows.", + "key": "replay_recording_duration_minimum", + "name": "Minimum duration", + "description": "Only record sessions longer than the minimum duration.", "unit": null, "limit": null, "note": null }, { - "key": "statistical_analysis", - "name": "Statistical analysis", - "description": "Get a statistical analysis of your experiment results to see if the results are significant, or if they're likely just due to chance.", + "key": "replay_feature_flag_based_recording", + "name": "Record via feature flag", + "description": "Only record sessions for users that have the flag enabled.", "unit": null, "limit": null, "note": null @@ -1341,7 +1635,7 @@ { "flat_amount_usd": "0", "unit_amount_usd": "0", - "up_to": 1000000, + "up_to": 5000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1349,8 +1643,8 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.0001", - "up_to": 2000000, + "unit_amount_usd": "0.04", + "up_to": 15000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1358,8 +1652,8 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000045", - "up_to": 10000000, + "unit_amount_usd": "0.003", + "up_to": 50000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1367,8 +1661,8 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.000025", - "up_to": 50000000, + "unit_amount_usd": "0.0027", + "up_to": 150000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1376,7 +1670,16 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.00001", + "unit_amount_usd": "0.0025", + "up_to": 500000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.002", "up_to": null, "current_amount_usd": "0.00", "current_usage": 0, @@ -1385,224 +1688,472 @@ } ], "current_plan": false, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], - "type": "feature_flags", - "free_allocation": 1000000, + "type": "session_replay", + "free_allocation": 5000, "tiers": null, "tiered": true, "unit_amount_usd": null, "current_amount_usd_before_addons": null, "current_amount_usd": null, "current_usage": 0, - "usage_limit": 1000000, + "usage_limit": 5000, "has_exceeded_limit": false, - "percentage_usage": 0, + "percentage_usage": 0.0, "projected_usage": 0, "projected_amount_usd": null, - "unit": "request", + "unit": "recording", "addons": [], "contact_support": false, - "inclusion_only": false + "inclusion_only": false, + "features": [ + { + "key": "recordings_playlists", + "name": "Recording playlists", + "description": "Create playlists of certain session recordings to easily find and watch them again in the future.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "session_replay_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "console_logs", + "name": "Console logs", + "description": "Debug issues faster by browsing the user's console.", + "images": { + "light": "https://posthog.com/images/products/session-replay/console.png", + "dark": "https://posthog.com/images/products/session-replay/console-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "recordings_performance", + "name": "Network performance on recordings", + "description": "See your end-user's network performance and information alongside session recordings.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "session_replay_network_payloads", + "name": "Network monitor", + "description": "Analyze performance and network calls.", + "images": { + "light": "https://posthog.com/images/products/session-replay/network.png", + "dark": "https://posthog.com/images/products/session-replay/network-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "recordings_file_export", + "name": "Download recordings", + "description": "Retain recordings beyond data retention limits.", + "images": null, + "icon_key": "IconDownload", + "type": "secondary" + }, + { + "key": "session_replay_sampling", + "name": "Sample recorded sessions", + "description": "Restrict the percentage of sessions that will be recorded.", + "images": null, + "icon_key": "IconSampling", + "type": "secondary" + }, + { + "key": "replay_recording_duration_minimum", + "name": "Minimum duration", + "description": "Only record sessions longer than the minimum duration.", + "images": null, + "icon_key": "IconClock", + "type": "secondary" + }, + { + "key": "replay_feature_flag_based_recording", + "name": "Record via feature flag", + "description": "Only record sessions for users that have the flag enabled.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "replay_mask_sensitive_data", + "name": "Block sensitive data", + "description": "Disable capturing data from any DOM element with HTML attributes or a customizable config.", + "images": null, + "icon_key": "IconPassword", + "type": "secondary" + }, + { + "key": "replay_sharing_embedding", + "name": "Share and embed", + "description": "Share replays directly via URL or embed via iframe.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "replay_product_analytics_integration", + "name": "Event timeline", + "description": "See a history of everything that happened in a user's session.", + "images": { + "light": "https://posthog.com/images/products/session-replay/timeline.png", + "dark": "https://posthog.com/images/products/session-replay/timeline-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "replay_filter_person_properties", + "name": "Filter person properties", + "description": "Filter by person properties to quickly find relevant recordings.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "replay_filter_events", + "name": "Filter events", + "description": "Filter by events to quickly find relevant recordings.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "replay_dom_explorer", + "name": "DOM Explorer", + "description": "Freeze snapshots of recordings and explore the DOM with your browser dev tools.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "works_with_posthog_js", + "name": "Capture sessions without extra code", + "description": "Works with PostHog.js", + "images": null, + "icon_key": "IconBolt", + "type": "secondary" + }, + { + "key": "replay_automatic_playlists", + "name": "Automatic playlists", + "description": "Filter by user behavior, user properties, or time.", + "images": null, + "icon_key": "IconPlaylist", + "type": "secondary" + } + ] }, { - "name": "Surveys", - "description": "Collect feedback from your users. Multiple choice, rating, open text, and more.", + "name": "Feature flags & A/B testing", + "headline": "Safely roll out features and A/B tests to specific users or groups", + "description": "Test changes with small groups of users before rolling out wider. Analyze usage with product analytics and session replay.", "price_description": null, - "usage_key": "survey_responses", - "image_url": "https://posthog.com/images/product/product-icons/surveys.svg", - "icon_key": "IconMessage", - "docs_url": "https://posthog.com/docs/surveys", + "usage_key": "feature_flag_requests", + "image_url": "https://posthog.com/images/products/feature-flags/feature-flags.png", + "screenshot_url": "https://posthog.com/images/products/feature-flags/screenshot-feature-flags.png", + "icon_key": "IconToggle", + "docs_url": "https://posthog.com/docs/feature-flags", "subscribed": false, "plans": [ { - "plan_key": "free-20230928", - "product_key": "surveys", - "name": "Surveys", - "description": "Collect feedback from your users. Multiple choice, rating, open text, and more.", - "image_url": "https://posthog.com/images/product/product-icons/surveys.svg", - "docs_url": "https://posthog.com/docs/surveys", + "plan_key": "free-20230117", + "product_key": "feature_flags", + "name": "Free", + "description": "Test changes with small groups of users before rolling out wider. Analyze usage with product analytics and session replay.", + "image_url": "https://posthog.com/images/products/feature-flags/feature-flags.png", + "docs_url": "https://posthog.com/docs/feature-flags", "note": null, - "unit": "survey response", - "free_allocation": 250, + "unit": "request", + "free_allocation": 1000000, "features": [ { - "key": "surveys_unlimited_surveys", - "name": "Unlimited surveys", - "description": "Create as many surveys as you want.", + "key": "boolean_flags", + "name": "Boolean feature flags", + "description": "Turn features on and off for specific users.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_all_question_types", - "name": "All question types", - "description": "Rating scale (for NPS and the like), multiple choice, single choice, emoji rating, link, free text.", + "key": "multivariate_flags", + "name": "Multivariate feature flags & experiments", + "description": "Create three or more variants of a feature flag to test or release different versions of a feature.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_user_targeting", - "name": "User property targeting", - "description": "Target users based on any of their user properties.", + "key": "persist_flags_cross_authentication", + "name": "Persist flags across authentication", + "description": "Persist feature flags across authentication events so that flag values don't change when an anonymous user logs in and becomes identified.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_user_sampling", - "name": "User sampling", - "description": "Sample users to only survey a portion of the users who match the criteria.", + "key": "feature_flag_payloads", + "name": "Test changes without code", + "description": "Use JSON payloads to change text, visuals, or entire blocks of code without subsequent deployments.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_api_mode", - "name": "API mode", - "description": "Create surveys via the API.", + "key": "multiple_release_conditions", + "name": "Multiple release conditions", + "description": "Customize your rollout strategy by user or group properties, cohort, or trafic percentage.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_results_analysis", - "name": "Results analysis", - "description": "Analyze your survey results including completion rates and drop offs.", + "key": "release_condition_overrides", + "name": "Release condition overrides", + "description": "For any release condition, specify which flag value the users or groups in that condition should receive.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_templates", - "name": "Templates", - "description": "Use our templates to get started quickly with NPS, customer satisfaction surveys, user interviews, and more.", + "key": "targeting_by_group", + "name": "Flag targeting by groups", + "description": "Target feature flag release conditions by group properties, not just user properties.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "local_evaluation_and_bootstrapping", + "name": "Local evaluation & bootstrapping", + "description": "Bootstrap flags on initialization so all flags are available immediately, without having to make extra network requests.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "flag_usage_stats", + "name": "Flag usage stats", + "description": "See how many times a flag has been evaluated, how many times each variant has been returned, and what values users received.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "experimentation", + "name": "A/B testing", + "description": "Test changes to your product and evaluate the impacts those changes make.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "funnel_experiments", + "name": "Funnel & trend experiments", + "description": "Measure the impact of a change on a aggregate values or a series of events, like a signup flow.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "secondary_metrics", + "name": "Secondary experiment metrics", + "description": "Track additional metrics to see how your experiment affects other parts of your app or different flows.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "statistical_analysis", + "name": "Statistical analysis", + "description": "Get a statistical analysis of your experiment results to see if the results are significant, or if they're likely just due to chance.", "unit": null, "limit": null, "note": null + }, + { + "key": "feature_flags_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "year", + "limit": 1, + "note": null } ], "tiers": null, "current_plan": true, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null }, { - "plan_key": "paid-20230928", - "product_key": "surveys", - "name": "Surveys", - "description": "Collect feedback from your users. Multiple choice, rating, open text, and more.", - "image_url": "https://posthog.com/images/product/product-icons/surveys.svg", - "docs_url": "https://posthog.com/docs/surveys", + "plan_key": "paid-20230623", + "product_key": "feature_flags", + "name": "Paid", + "description": "Test changes with small groups of users before rolling out wider. Analyze usage with product analytics and session replay.", + "image_url": "https://posthog.com/images/products/feature-flags/feature-flags.png", + "docs_url": "https://posthog.com/docs/feature-flags", "note": null, - "unit": "survey response", + "unit": "request", "free_allocation": null, "features": [ { - "key": "surveys_unlimited_surveys", - "name": "Unlimited surveys", - "description": "Create as many surveys as you want.", + "key": "boolean_flags", + "name": "Boolean feature flags", + "description": "Turn features on and off for specific users.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_all_question_types", - "name": "All question types", - "description": "Rating scale (for NPS and the like), multiple choice, single choice, emoji rating, link, free text.", + "key": "multivariate_flags", + "name": "Multivariate feature flags & experiments", + "description": "Create three or more variants of a feature flag to test or release different versions of a feature.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_multiple_questions", - "name": "Multiple questions", - "description": "Create multiple questions in a single survey.", + "key": "persist_flags_cross_authentication", + "name": "Persist flags across authentication", + "description": "Persist feature flags across authentication events so that flag values don't change when an anonymous user logs in and becomes identified.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_user_targeting", - "name": "User property targeting", - "description": "Target users based on any of their user properties.", + "key": "feature_flag_payloads", + "name": "Test changes without code", + "description": "Use JSON payloads to change text, visuals, or entire blocks of code without subsequent deployments.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_user_sampling", - "name": "User sampling", - "description": "Sample users to only survey a portion of the users who match the criteria.", + "key": "multiple_release_conditions", + "name": "Multiple release conditions", + "description": "Customize your rollout strategy by user or group properties, cohort, or trafic percentage.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_styling", - "name": "Custom colors & positioning", - "description": "Customize the colors of your surveys to match your brand and set survey position.", + "key": "release_condition_overrides", + "name": "Release condition overrides", + "description": "For any release condition, specify which flag value the users or groups in that condition should receive.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_text_html", - "name": "Custom HTML text", - "description": "Add custom HTML to your survey text.", + "key": "targeting_by_group", + "name": "Flag targeting by groups", + "description": "Target feature flag release conditions by group properties, not just user properties.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_api_mode", - "name": "API mode", - "description": "Create surveys via the API.", + "key": "local_evaluation_and_bootstrapping", + "name": "Local evaluation & bootstrapping", + "description": "Bootstrap flags on initialization so all flags are available immediately, without having to make extra network requests.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_results_analysis", - "name": "Results analysis", - "description": "Analyze your survey results including completion rates and drop offs.", + "key": "flag_usage_stats", + "name": "Flag usage stats", + "description": "See how many times a flag has been evaluated, how many times each variant has been returned, and what values users received.", "unit": null, "limit": null, "note": null }, { - "key": "surveys_templates", - "name": "Templates", - "description": "Use our templates to get started quickly with NPS, customer satisfaction surveys, user interviews, and more.", + "key": "experimentation", + "name": "A/B testing", + "description": "Test changes to your product and evaluate the impacts those changes make.", "unit": null, "limit": null, "note": null - } - ], - "tiers": [ + }, { - "flat_amount_usd": "0", - "unit_amount_usd": "0", - "up_to": 250, - "current_amount_usd": "0.00", - "current_usage": 0, - "projected_usage": null, - "projected_amount_usd": null + "key": "funnel_experiments", + "name": "Funnel & trend experiments", + "description": "Measure the impact of a change on a aggregate values or a series of events, like a signup flow.", + "unit": null, + "limit": null, + "note": null }, { - "flat_amount_usd": "0", - "unit_amount_usd": "0.2", - "up_to": 500, - "current_amount_usd": "0.00", + "key": "secondary_metrics", + "name": "Secondary experiment metrics", + "description": "Track additional metrics to see how your experiment affects other parts of your app or different flows.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "statistical_analysis", + "name": "Statistical analysis", + "description": "Get a statistical analysis of your experiment results to see if the results are significant, or if they're likely just due to chance.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "group_experiments", + "name": "Group experiments", + "description": "Target experiments to specific groups of users so everyone in the same group gets the same variant.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "multiple_environments", + "name": "Multi-environment support", + "description": "Test flags in local development or staging by using the same flag key across PostHog projects.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "feature_flags_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "years", + "limit": 7, + "note": null + } + ], + "tiers": [ + { + "flat_amount_usd": "0", + "unit_amount_usd": "0", + "up_to": 1000000, + "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, "projected_amount_usd": null }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.1", - "up_to": 1000, + "unit_amount_usd": "0.0001", + "up_to": 2000000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1610,8 +2161,8 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.035", - "up_to": 10000, + "unit_amount_usd": "0.000045", + "up_to": 10000000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1619,8 +2170,8 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.015", - "up_to": 20000, + "unit_amount_usd": "0.000025", + "up_to": 50000000, "current_amount_usd": "0.00", "current_usage": 0, "projected_usage": null, @@ -1628,7 +2179,7 @@ }, { "flat_amount_usd": "0", - "unit_amount_usd": "0.01", + "unit_amount_usd": "0.00001", "up_to": null, "current_amount_usd": "0.00", "current_usage": 0, @@ -1637,224 +2188,612 @@ } ], "current_plan": false, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null } ], - "type": "surveys", - "free_allocation": 250, + "type": "feature_flags", + "free_allocation": 1000000, "tiers": null, "tiered": true, "unit_amount_usd": null, "current_amount_usd_before_addons": null, "current_amount_usd": null, "current_usage": 0, - "usage_limit": 250, + "usage_limit": 1000000, "has_exceeded_limit": false, - "percentage_usage": 0, + "percentage_usage": 0.0, "projected_usage": 0, "projected_amount_usd": null, - "unit": "survey response", + "unit": "request", "addons": [], "contact_support": false, - "inclusion_only": false + "inclusion_only": false, + "features": [ + { + "key": "boolean_flags", + "name": "Boolean feature flags", + "description": "Turn features on and off for specific users.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "feature_flags_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "multivariate_flags", + "name": "Multivariate feature flags & experiments", + "description": "Create three or more variants of a feature flag to test or release different versions of a feature.", + "images": { + "light": "https://posthog.com/images/products/feature-flags/multivariate.png", + "dark": "https://posthog.com/images/products/feature-flags/multivariate-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "persist_flags_cross_authentication", + "name": "Persist flags across authentication", + "description": "Persist feature flags across authentication events so that flag values don't change when an anonymous user logs in and becomes identified.", + "images": null, + "icon_key": "IconUnlock", + "type": "secondary" + }, + { + "key": "feature_flag_payloads", + "name": "Test changes without code", + "description": "Use JSON payloads to change text, visuals, or entire blocks of code without subsequent deployments.", + "images": { + "light": "https://posthog.com/images/products/feature-flags/payloads.png", + "dark": "https://posthog.com/images/products/feature-flags/payloads-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "multiple_release_conditions", + "name": "Multiple release conditions", + "description": "Customize your rollout strategy by user or group properties, cohort, or trafic percentage.", + "images": { + "light": "https://posthog.com/images/products/feature-flags/release-conditions.png", + "dark": "https://posthog.com/images/products/feature-flags/release-conditions-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "release_condition_overrides", + "name": "Release condition overrides", + "description": "For any release condition, specify which flag value the users or groups in that condition should receive.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "targeting_by_group", + "name": "Flag targeting by groups", + "description": "Target feature flag release conditions by group properties, not just user properties.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "local_evaluation_and_bootstrapping", + "name": "Local evaluation & bootstrapping", + "description": "Bootstrap flags on initialization so all flags are available immediately, without having to make extra network requests.", + "images": null, + "icon_key": "IconDecisionTree", + "type": "secondary" + }, + { + "key": "flag_usage_stats", + "name": "Flag usage stats", + "description": "See how many times a flag has been evaluated, how many times each variant has been returned, and what values users received.", + "images": { + "light": "https://posthog.com/images/products/feature-flags/reports.png", + "dark": "https://posthog.com/images/products/feature-flags/reports-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "multiple_environments", + "name": "Multi-environment support", + "description": "Test flags in local development or staging by using the same flag key across PostHog projects.", + "images": null, + "icon_key": "IconStack", + "type": "secondary" + }, + { + "key": "user_opt_in", + "name": "Early access feature opt-in widget", + "description": "Allow users to opt in to (or out of) specified features. Or use the API to build your own UI.", + "images": { + "light": "https://posthog.com/images/products/feature-flags/early-access.png", + "dark": "https://posthog.com/images/products/feature-flags/early-access-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "instant_rollbacks", + "name": "Instant rollbacks", + "description": "Disable a feature without touching your codebase.", + "images": null, + "icon_key": "IconRevert", + "type": "secondary" + }, + { + "key": "experimentation", + "name": "A/B testing", + "description": "Test changes to your product and evaluate the impacts those changes make.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "group_experiments", + "name": "Group experiments", + "description": "Target experiments to specific groups of users so everyone in the same group gets the same variant.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "funnel_experiments", + "name": "Funnel & trend experiments", + "description": "Measure the impact of a change on a aggregate values or a series of events, like a signup flow.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "secondary_metrics", + "name": "Secondary experiment metrics", + "description": "Track additional metrics to see how your experiment affects other parts of your app or different flows.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "statistical_analysis", + "name": "Statistical analysis", + "description": "Get a statistical analysis of your experiment results to see if the results are significant, or if they're likely just due to chance.", + "images": null, + "icon_key": null, + "type": null + } + ] }, { - "name": "Data warehouse", - "description": "A single source for all your important data. This product is in beta. Pricing will be changing.", + "name": "Surveys", + "headline": "Ask anything with no-code surveys", + "description": "Build in-app popups with freeform text responses, multiple choice, NPS, ratings, and emoji reactions. Or use the API for complete control.", "price_description": null, - "usage_key": "synced_rows", - "image_url": "https://posthog.com/images/product/product-icons/data-warehouse.svg", - "icon_key": "IconServer", - "docs_url": "https://posthog.com/docs/data-warehouse", + "usage_key": "survey_responses", + "image_url": "https://posthog.com/images/products/surveys/surveys.png", + "screenshot_url": "https://posthog.com/images/products/surveys/screenshot-surveys.png", + "icon_key": "IconMessage", + "docs_url": "https://posthog.com/docs/surveys", "subscribed": false, "plans": [ { - "plan_key": "free-20231026", - "product_key": "data_warehouse", - "name": "Data warehouse", - "description": "A single source for all your important data. This product is in beta. Pricing will be changing.", - "image_url": "https://posthog.com/images/product/product-icons/data-warehouse.svg", - "docs_url": "https://posthog.com/docs/data-warehouse", + "plan_key": "free-20230928", + "product_key": "surveys", + "name": "Free", + "description": "Build in-app popups with freeform text responses, multiple choice, NPS, ratings, and emoji reactions. Or use the API for complete control.", + "image_url": "https://posthog.com/images/products/surveys/surveys.png", + "docs_url": "https://posthog.com/docs/surveys", "note": null, - "unit": "row", - "free_allocation": null, + "unit": "survey response", + "free_allocation": 250, "features": [ { - "key": "data_warehouse_manual_sync", - "name": "Manual sync", - "description": "Sync your data to the warehouse using your cloud storage provider.", + "key": "surveys_unlimited_surveys", + "name": "Unlimited surveys", + "description": "Create as many surveys as you want.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "surveys_all_question_types", + "name": "All question types", + "description": "Rating scale (for NPS and the like), multiple choice, single choice, emoji rating, link, free text.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "surveys_user_targeting", + "name": "Advanced user targeting", + "description": "Target by URL, user property, or feature flag when used with Feature flags.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "surveys_user_sampling", + "name": "User sampling", + "description": "Sample users to only survey a portion of the users who match the criteria.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "surveys_api_mode", + "name": "API mode", + "description": "Using PostHog.js? No more code required. But if want to create your own UI, we have a full API.", "unit": null, "limit": null, "note": null }, { - "key": "data_warehouse_unified_querying", - "name": "Unified querying", - "description": "Query all your business and product data directly inside PostHog.", + "key": "surveys_results_analysis", + "name": "Aggregated results", + "description": "See feedback summarized and broken down per response, plus completion rates and drop offs.", "unit": null, "limit": null, "note": null }, { - "key": "data_warehouse_insights_visualization", - "name": "Insights", - "description": "Create insights from the data you import and add them to your PostHog dashboards.", + "key": "surveys_templates", + "name": "Templates", + "description": "Use our templates to get started quickly with NPS, customer satisfaction surveys, user interviews, and more.", "unit": null, "limit": null, "note": null + }, + { + "key": "surveys_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "year", + "limit": 1, + "note": null } ], "tiers": null, "current_plan": true, - "included_if": null + "included_if": null, + "contact_support": null, + "unit_amount_usd": null }, { - "plan_key": "paid-20231026", - "product_key": "data_warehouse", - "name": "Data warehouse", - "description": "A single source for all your important data. This product is in beta. Pricing will be changing.", - "image_url": "https://posthog.com/images/product/product-icons/data-warehouse.svg", - "docs_url": "https://posthog.com/docs/data-warehouse", + "plan_key": "paid-20230928", + "product_key": "surveys", + "name": "Paid", + "description": "Build in-app popups with freeform text responses, multiple choice, NPS, ratings, and emoji reactions. Or use the API for complete control.", + "image_url": "https://posthog.com/images/products/surveys/surveys.png", + "docs_url": "https://posthog.com/docs/surveys", "note": null, - "unit": "row", + "unit": "survey response", "free_allocation": null, "features": [ { - "key": "data_warehouse_manual_sync", - "name": "Manual sync", - "description": "Sync your data to the warehouse using your cloud storage provider.", + "key": "surveys_unlimited_surveys", + "name": "Unlimited surveys", + "description": "Create as many surveys as you want.", "unit": null, "limit": null, "note": null }, { - "key": "data_warehouse_one_click_sync", - "name": "One-Click sync", - "description": "Sync your data to the warehouse with one click.", + "key": "surveys_all_question_types", + "name": "All question types", + "description": "Rating scale (for NPS and the like), multiple choice, single choice, emoji rating, link, free text.", "unit": null, "limit": null, "note": null }, { - "key": "data_warehouse_unified_querying", - "name": "Unified querying", - "description": "Query all your business and product data directly inside PostHog.", + "key": "surveys_multiple_questions", + "name": "Multiple questions", + "description": "Ask up to 10 questions in a single survey.", "unit": null, "limit": null, "note": null }, { - "key": "data_warehouse_insights_visualization", - "name": "Insights", - "description": "Create insights from the data you import and add them to your PostHog dashboards.", + "key": "surveys_user_targeting", + "name": "Advanced user targeting", + "description": "Target by URL, user property, or feature flag when used with Feature flags.", "unit": null, "limit": null, "note": null - } - ], - "tiers": [ + }, { - "flat_amount_usd": "0", - "unit_amount_usd": "0.000015", - "up_to": null, - "current_amount_usd": "0.00", - "current_usage": 0, - "projected_usage": null, - "projected_amount_usd": null - } - ], - "current_plan": false, - "included_if": null - } - ], - "type": "data_warehouse", - "free_allocation": 0, - "tiers": null, - "tiered": false, - "unit_amount_usd": null, - "current_amount_usd_before_addons": null, - "current_amount_usd": null, - "current_usage": 0, - "usage_limit": 0, - "has_exceeded_limit": false, - "percentage_usage": 0, - "projected_usage": 0, - "projected_amount_usd": null, - "unit": "row", - "addons": [], - "contact_support": false, - "inclusion_only": false - }, - { - "name": "Integrations + CDP", - "description": "Connect PostHog to your favorite tools.", - "price_description": null, - "usage_key": null, - "image_url": "https://posthog.com/images/product/product-icons/integrations.svg", - "icon_key": "IconBolt", - "docs_url": "https://posthog.com/docs/apps", - "subscribed": null, - "plans": [ - { - "plan_key": "free-20230117", - "product_key": "integrations", - "name": "Integrations + CDP", - "description": "Connect PostHog to your favorite tools.", - "image_url": "https://posthog.com/images/product/product-icons/integrations.svg", - "docs_url": "https://posthog.com/docs/apps", - "note": null, - "unit": null, - "free_allocation": null, - "features": [ + "key": "surveys_user_sampling", + "name": "User sampling", + "description": "Sample users to only survey a portion of the users who match the criteria.", + "unit": null, + "limit": null, + "note": null + }, { - "key": "zapier", - "name": "Zapier", - "description": "Zapier lets you connect PostHog with thousands of the most popular apps, so you can automate your work and have more time for what matters most—no code required.", + "key": "surveys_styling", + "name": "Custom colors & positioning", + "description": "Customize the colors of your surveys to match your brand and set survey position.", "unit": null, "limit": null, "note": null }, { - "key": "slack_integration", - "name": "Slack", - "description": "Get notified about new actions in Slack.", + "key": "surveys_text_html", + "name": "Custom HTML text", + "description": "Add custom HTML to your survey text.", "unit": null, "limit": null, "note": null }, { - "key": "microsoft_teams_integration", - "name": "Microsoft Teams", - "description": "Get notified about new actions in Microsoft Teams.", + "key": "surveys_api_mode", + "name": "API mode", + "description": "Using PostHog.js? No more code required. But if want to create your own UI, we have a full API.", "unit": null, "limit": null, "note": null }, { - "key": "discord_integration", - "name": "Discord", - "description": "Get notified about new actions in Discord.", + "key": "surveys_results_analysis", + "name": "Aggregated results", + "description": "See feedback summarized and broken down per response, plus completion rates and drop offs.", "unit": null, "limit": null, "note": null }, { - "key": "apps", - "name": "CDP + Apps library", - "description": "Connect your data with 50+ apps including BigQuery, Redshift, and more.", + "key": "surveys_templates", + "name": "Templates", + "description": "Use our templates to get started quickly with NPS, customer satisfaction surveys, user interviews, and more.", "unit": null, "limit": null, "note": null + }, + { + "key": "surveys_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "unit": "years", + "limit": 7, + "note": null } ], - "tiers": null, - "current_plan": false, - "included_if": "no_active_subscription" - }, - { - "plan_key": "paid-20230117", + "tiers": [ + { + "flat_amount_usd": "0", + "unit_amount_usd": "0", + "up_to": 250, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.2", + "up_to": 500, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.1", + "up_to": 1000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.035", + "up_to": 10000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.015", + "up_to": 20000, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + }, + { + "flat_amount_usd": "0", + "unit_amount_usd": "0.01", + "up_to": null, + "current_amount_usd": "0.00", + "current_usage": 0, + "projected_usage": null, + "projected_amount_usd": null + } + ], + "current_plan": false, + "included_if": null, + "contact_support": null, + "unit_amount_usd": null + } + ], + "type": "surveys", + "free_allocation": 250, + "tiers": null, + "tiered": true, + "unit_amount_usd": null, + "current_amount_usd_before_addons": null, + "current_amount_usd": null, + "current_usage": 0, + "usage_limit": 250, + "has_exceeded_limit": false, + "percentage_usage": 0.0, + "projected_usage": 0, + "projected_amount_usd": null, + "unit": "survey response", + "addons": [], + "contact_support": false, + "inclusion_only": false, + "features": [ + { + "key": "surveys_unlimited_surveys", + "name": "Unlimited surveys", + "description": "Create as many surveys as you want.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "surveys_all_question_types", + "name": "All question types", + "description": "Rating scale (for NPS and the like), multiple choice, single choice, emoji rating, link, free text.", + "images": { + "light": "https://posthog.com/images/products/surveys/question-types.png", + "dark": "https://posthog.com/images/products/surveys/question-types-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "surveys_multiple_questions", + "name": "Multiple questions", + "description": "Ask up to 10 questions in a single survey.", + "images": { + "light": "https://posthog.com/images/products/surveys/steps.png", + "dark": "https://posthog.com/images/products/surveys/steps-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "surveys_user_targeting", + "name": "Advanced user targeting", + "description": "Target by URL, user property, or feature flag when used with Feature flags.", + "images": { + "light": "https://posthog.com/images/products/surveys/targeting.png", + "dark": "https://posthog.com/images/products/surveys/targeting-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "surveys_user_sampling", + "name": "User sampling", + "description": "Sample users to only survey a portion of the users who match the criteria.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "surveys_styling", + "name": "Custom colors & positioning", + "description": "Customize the colors of your surveys to match your brand and set survey position.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "surveys_text_html", + "name": "Custom HTML text", + "description": "Add custom HTML to your survey text.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "surveys_api_mode", + "name": "API mode", + "description": "Using PostHog.js? No more code required. But if want to create your own UI, we have a full API.", + "images": { + "light": "https://posthog.com/images/products/surveys/api.png", + "dark": "https://posthog.com/images/products/surveys/api-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "surveys_results_analysis", + "name": "Aggregated results", + "description": "See feedback summarized and broken down per response, plus completion rates and drop offs.", + "images": null, + "icon_key": "IconPieChart", + "type": "secondary" + }, + { + "key": "surveys_templates", + "name": "Templates", + "description": "Use our templates to get started quickly with NPS, customer satisfaction surveys, user interviews, and more.", + "images": { + "light": "https://posthog.com/images/products/surveys/templates.png", + "dark": "https://posthog.com/images/products/surveys/templates-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "surveys_data_retention", + "name": "Data retention", + "description": "Keep a historical record of your data.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "surveys_link_question_type", + "name": "Link somewhere", + "description": "Send users to a webpage or invite them to book a meeting with a calendar invite.", + "images": { + "light": "https://posthog.com/images/products/surveys/link-scheduler.png", + "dark": "https://posthog.com/images/products/surveys/link-scheduler-dark.png" + }, + "icon_key": null, + "type": "primary" + }, + { + "key": "surveys_slack_notifications", + "name": "Slack notifications", + "description": "Send realtime survey responses to a Slack channel.", + "images": null, + "icon_key": "IconNotification", + "type": "secondary" + }, + { + "key": "surveys_wait_periods", + "name": "Customizable wait periods", + "description": "Set a delay before a survey opens.", + "images": null, + "icon_key": "IconClock", + "type": "secondary" + } + ] + }, + { + "name": "Integrations", + "headline": null, + "description": "Connect PostHog to your favorite tools.", + "price_description": null, + "usage_key": null, + "image_url": "https://posthog.com/images/product/product-icons/integrations.svg", + "screenshot_url": null, + "icon_key": "IconBolt", + "docs_url": "https://posthog.com/docs/apps", + "subscribed": null, + "plans": [ + { + "plan_key": "free-20230117", "product_key": "integrations", - "name": "Integrations + CDP", + "name": "Free", "description": "Connect PostHog to your favorite tools.", "image_url": "https://posthog.com/images/product/product-icons/integrations.svg", "docs_url": "https://posthog.com/docs/apps", @@ -1871,83 +2810,545 @@ "note": null }, { - "key": "slack_integration", - "name": "Slack", - "description": "Get notified about new actions in Slack.", + "key": "slack_integration", + "name": "Slack", + "description": "Get notified about new actions in Slack.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "microsoft_teams_integration", + "name": "Microsoft Teams", + "description": "Get notified about new actions in Microsoft Teams.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "discord_integration", + "name": "Discord", + "description": "Get notified about new actions in Discord.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "apps", + "name": "Apps", + "description": "Use apps to transform, filter, and modify your incoming data. (Export apps not included, see the Data pipelines addon for product analytics.)", + "unit": null, + "limit": null, + "note": null + } + ], + "tiers": null, + "current_plan": false, + "included_if": "no_active_subscription", + "contact_support": null, + "unit_amount_usd": null + }, + { + "plan_key": "paid-20230117", + "product_key": "integrations", + "name": "Paid", + "description": "Connect PostHog to your favorite tools.", + "image_url": "https://posthog.com/images/product/product-icons/integrations.svg", + "docs_url": "https://posthog.com/docs/apps", + "note": null, + "unit": null, + "free_allocation": null, + "features": [ + { + "key": "zapier", + "name": "Zapier", + "description": "Zapier lets you connect PostHog with thousands of the most popular apps, so you can automate your work and have more time for what matters most—no code required.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "slack_integration", + "name": "Slack", + "description": "Get notified about new actions in Slack.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "microsoft_teams_integration", + "name": "Microsoft Teams", + "description": "Get notified about new actions in Microsoft Teams.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "discord_integration", + "name": "Discord", + "description": "Get notified about new actions in Discord.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "apps", + "name": "Apps", + "description": "Use apps to transform, filter, and modify your incoming data. (Export apps not included, see the Data pipelines addon for product analytics.)", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "app_metrics", + "name": "App metrics", + "description": "Get metrics on your apps to see their usage, reliability, and more.", + "unit": null, + "limit": null, + "note": null + } + ], + "tiers": null, + "current_plan": true, + "included_if": "has_subscription", + "contact_support": null, + "unit_amount_usd": null + } + ], + "type": "integrations", + "free_allocation": 0, + "tiers": null, + "tiered": false, + "unit_amount_usd": null, + "current_amount_usd_before_addons": null, + "current_amount_usd": null, + "current_usage": 0, + "usage_limit": 0, + "has_exceeded_limit": false, + "percentage_usage": 0, + "projected_usage": 0, + "projected_amount_usd": null, + "unit": null, + "addons": [], + "contact_support": false, + "inclusion_only": true, + "features": [ + { + "key": "apps", + "name": "Apps", + "description": "Use apps to transform, filter, and modify your incoming data. (Export apps not included, see the Data pipelines addon for product analytics.)", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "slack_integration", + "name": "Slack", + "description": "Get notified about new actions in Slack.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "microsoft_teams_integration", + "name": "Microsoft Teams", + "description": "Get notified about new actions in Microsoft Teams.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "discord_integration", + "name": "Discord", + "description": "Get notified about new actions in Discord.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "zapier", + "name": "Zapier", + "description": "Zapier lets you connect PostHog with thousands of the most popular apps, so you can automate your work and have more time for what matters most—no code required.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "app_metrics", + "name": "App metrics", + "description": "Get metrics on your apps to see their usage, reliability, and more.", + "images": null, + "icon_key": null, + "type": null + } + ] + }, + { + "name": "Platform and support", + "headline": null, + "description": "SSO, permission management, and support.", + "price_description": null, + "usage_key": null, + "image_url": "https://posthog.com/images/product/product-icons/platform.svg", + "screenshot_url": null, + "icon_key": "IconStack", + "docs_url": "https://posthog.com/docs", + "subscribed": null, + "plans": [ + { + "plan_key": "free-20230117", + "product_key": "platform_and_support", + "name": "Totally free", + "description": "SSO, permission management, and support.", + "image_url": "https://posthog.com/images/product/product-icons/platform.svg", + "docs_url": "https://posthog.com/docs", + "note": null, + "unit": null, + "free_allocation": null, + "features": [ + { + "key": "tracked_users", + "name": "Tracked users", + "description": "Track users across devices and sessions.", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "team_members", + "name": "Team members", + "description": "PostHog doesn't charge per seat add your entire team!", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "organizations_projects", + "name": "Projects", + "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", + "unit": "project", + "limit": 1, + "note": null + }, + { + "key": "api_access", + "name": "API access", + "description": "Access your data via our developer-friendly API.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "social_sso", + "name": "SSO via Google, Github, or Gitlab", + "description": "Log in to PostHog with your Google, Github, or Gitlab account.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "community_support", + "name": "Community support", + "description": "Get help from other users and PostHog team members in our Community forums.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", + "unit": null, + "limit": null, + "note": null + } + ], + "tiers": null, + "current_plan": false, + "included_if": "no_active_subscription", + "contact_support": null, + "unit_amount_usd": null + }, + { + "plan_key": "paid-20240208", + "product_key": "platform_and_support", + "name": "With subscription", + "description": "SSO, permission management, and support.", + "image_url": "https://posthog.com/images/product/product-icons/platform.svg", + "docs_url": "https://posthog.com/docs", + "note": null, + "unit": null, + "free_allocation": null, + "features": [ + { + "key": "tracked_users", + "name": "Tracked users", + "description": "Track users across devices and sessions.", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "team_members", + "name": "Team members", + "description": "PostHog doesn't charge per seat add your entire team!", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "organizations_projects", + "name": "Projects", + "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", + "unit": "projects", + "limit": 2, + "note": null + }, + { + "key": "api_access", + "name": "API access", + "description": "Access your data via our developer-friendly API.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "social_sso", + "name": "SSO via Google, Github, or Gitlab", + "description": "Log in to PostHog with your Google, Github, or Gitlab account.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "community_support", + "name": "Community support", + "description": "Get help from other users and PostHog team members in our Community forums.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "dedicated_support", + "name": "Dedicated account manager", + "description": "Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.", + "unit": null, + "limit": null, + "note": "$2k+/month spend" + }, + { + "key": "email_support", + "name": "Email support", + "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", + "unit": null, + "limit": null, + "note": null + } + ], + "tiers": null, + "current_plan": true, + "included_if": "has_subscription", + "contact_support": null, + "unit_amount_usd": null + }, + { + "plan_key": "teams-20240208", + "product_key": "platform_and_support", + "name": "Teams", + "description": "SSO, permission management, and support.", + "image_url": "https://posthog.com/images/product/product-icons/platform.svg", + "docs_url": "https://posthog.com/docs", + "note": null, + "unit": null, + "free_allocation": null, + "features": [ + { + "key": "tracked_users", + "name": "Tracked users", + "description": "Track users across devices and sessions.", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "team_members", + "name": "Team members", + "description": "PostHog doesn't charge per seat add your entire team!", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "organizations_projects", + "name": "Projects", + "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "api_access", + "name": "API access", + "description": "Access your data via our developer-friendly API.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "social_sso", + "name": "SSO via Google, Github, or Gitlab", + "description": "Log in to PostHog with your Google, Github, or Gitlab account.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "sso_enforcement", + "name": "Enforce SSO login", + "description": "Users can only sign up and log in to your PostHog organization with your specified SSO provider.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "2fa_enforcement", + "name": "Enforce 2FA", + "description": "Require all users in your organization to enable two-factor authentication.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "community_support", + "name": "Community support", + "description": "Get help from other users and PostHog team members in our Community forums.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "email_support", + "name": "Email support", + "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "dedicated_support", + "name": "Dedicated account manager", + "description": "Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.", + "unit": null, + "limit": null, + "note": "$2k+/month spend" + }, + { + "key": "priority_support", + "name": "Priority support", + "description": "Get help from our team faster than other customers.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "white_labelling", + "name": "White labeling", + "description": "Use your own branding on surveys, shared dashboards, shared insights, and more.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "project_based_permissioning", + "name": "Project permissions", + "description": "Restrict access to data within the organization to only those who need it.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "advanced_permissions", + "name": "Advanced permissions", + "description": "Control who can access and modify data and features within your organization.", + "unit": null, + "limit": null, + "note": "Project-based only" + }, + { + "key": "audit_logs", + "name": "Audit logs", + "description": "See who in your organization has accessed or modified entities within PostHog.", + "unit": null, + "limit": null, + "note": "Basic" + }, + { + "key": "security_assessment", + "name": "Security assessment", + "description": "Security assessment", "unit": null, "limit": null, "note": null }, { - "key": "microsoft_teams_integration", - "name": "Microsoft Teams", - "description": "Get notified about new actions in Microsoft Teams.", + "key": "hipaa_baa", + "name": "HIPAA BAA", + "description": "Get a signed HIPAA Business Associate Agreement (BAA) to use PostHog in a HIPAA-compliant manner.", "unit": null, "limit": null, "note": null }, { - "key": "discord_integration", - "name": "Discord", - "description": "Get notified about new actions in Discord.", + "key": "team_collaboration", + "name": "Team collaboration features", + "description": "Work together better with tags on dashboards and insights; descriptions on insights, events, & properties; verified events; comments on almost anything.", "unit": null, "limit": null, "note": null }, { - "key": "apps", - "name": "CDP + Apps library", - "description": "Connect your data with 50+ apps including BigQuery, Redshift, and more.", + "key": "ingestion_taxonomy", + "name": "Ingestion taxonomy", + "description": "Mark events as verified or unverified to help you understand the quality of your data.", "unit": null, "limit": null, "note": null }, { - "key": "app_metrics", - "name": "App metrics", - "description": "Get metrics on your apps to see their usage, reliability, and more.", + "key": "tagging", + "name": "Dashboard tags", + "description": "Organize dashboards with tags.", "unit": null, "limit": null, "note": null } ], - "tiers": null, - "current_plan": true, - "included_if": "has_subscription" - } - ], - "type": "integrations", - "free_allocation": 0, - "tiers": null, - "tiered": false, - "unit_amount_usd": null, - "current_amount_usd_before_addons": null, - "current_amount_usd": null, - "current_usage": 0, - "usage_limit": 0, - "has_exceeded_limit": false, - "percentage_usage": 0, - "projected_usage": 0, - "projected_amount_usd": null, - "unit": null, - "addons": [], - "contact_support": false, - "inclusion_only": true - }, - { - "name": "Platform and support", - "description": "SSO, permission management, and support.", - "price_description": null, - "usage_key": null, - "image_url": "https://posthog.com/images/product/product-icons/platform.svg", - "icon_key": "IconStack", - "docs_url": "https://posthog.com/docs", - "subscribed": null, - "plans": [ + "tiers": [], + "current_plan": false, + "included_if": null, + "contact_support": null, + "unit_amount_usd": "450.00" + }, { - "plan_key": "free-20230117", + "plan_key": "enterprise-20240208", "product_key": "platform_and_support", - "name": "Platform and support", + "name": "Enterprise", "description": "SSO, permission management, and support.", "image_url": "https://posthog.com/images/product/product-icons/platform.svg", "docs_url": "https://posthog.com/docs", @@ -1955,6 +3356,22 @@ "unit": null, "free_allocation": null, "features": [ + { + "key": "team_members", + "name": "Team members", + "description": "PostHog doesn't charge per seat add your entire team!", + "unit": null, + "limit": null, + "note": "Unlimited" + }, + { + "key": "organizations_projects", + "name": "Projects", + "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", + "unit": null, + "limit": null, + "note": "Unlimited" + }, { "key": "tracked_users", "name": "Tracked users", @@ -1964,33 +3381,41 @@ "note": "Unlimited" }, { - "key": "data_retention", - "name": "Data retention", - "description": "Keep a historical record of your data.", - "unit": "year", - "limit": 1, + "key": "api_access", + "name": "API access", + "description": "Access your data via our developer-friendly API.", + "unit": null, + "limit": null, "note": null }, { - "key": "team_members", - "name": "Team members", - "description": "PostHog doesn't charge per seat add your entire team!", + "key": "white_labelling", + "name": "White labeling", + "description": "Use your own branding on surveys, shared dashboards, shared insights, and more.", "unit": null, "limit": null, - "note": "Unlimited" + "note": null }, { - "key": "organizations_projects", - "name": "Projects", - "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", - "unit": "project", - "limit": 1, + "key": "team_collaboration", + "name": "Team collaboration features", + "description": "Work together better with tags on dashboards and insights; descriptions on insights, events, & properties; verified events; comments on almost anything.", + "unit": null, + "limit": null, "note": null }, { - "key": "api_access", - "name": "API access", - "description": "Access your data via our developer-friendly API.", + "key": "ingestion_taxonomy", + "name": "Ingestion taxonomy", + "description": "Mark events as verified or unverified to help you understand the quality of your data.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "tagging", + "name": "Dashboard tags", + "description": "Organize dashboards with tags.", "unit": null, "limit": null, "note": null @@ -2004,97 +3429,81 @@ "note": null }, { - "key": "community_support", - "name": "Community support", - "description": "Get help from other users and PostHog team members in our Community forums.", + "key": "sso_enforcement", + "name": "Enforce SSO login", + "description": "Users can only sign up and log in to your PostHog organization with your specified SSO provider.", "unit": null, "limit": null, "note": null }, { - "key": "terms_and_conditions", - "name": "Terms and conditions", - "description": "Terms and conditions", + "key": "saml", + "name": "SAML SSO", + "description": "Allow your organization's users to log in with SAML.", "unit": null, "limit": null, - "note": "Standard" - } - ], - "tiers": null, - "current_plan": false, - "included_if": "no_active_subscription" - }, - { - "plan_key": "paid-20230926", - "product_key": "platform_and_support", - "name": "Platform and support", - "description": "SSO, permission management, and support.", - "image_url": "https://posthog.com/images/product/product-icons/platform.svg", - "docs_url": "https://posthog.com/docs", - "note": null, - "unit": null, - "free_allocation": null, - "features": [ + "note": null + }, { - "key": "tracked_users", - "name": "Tracked users", - "description": "Track users across devices and sessions.", + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", "unit": null, "limit": null, - "note": "Unlimited" + "note": null }, { - "key": "data_retention", - "name": "Data retention", - "description": "Keep a historical record of your data.", + "key": "2fa_enforcement", + "name": "Enforce 2FA", + "description": "Require all users in your organization to enable two-factor authentication.", "unit": null, "limit": null, - "note": "7 years" + "note": null }, { - "key": "team_members", - "name": "Team members", - "description": "PostHog doesn't charge per seat add your entire team!", + "key": "project_based_permissioning", + "name": "Project permissions", + "description": "Restrict access to data within the organization to only those who need it.", "unit": null, "limit": null, - "note": "Unlimited" + "note": null }, { - "key": "organizations_projects", - "name": "Projects", - "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", + "key": "role_based_access", + "name": "Role-based access", + "description": "Control access to features like experiments, session recordings, and feature flags with custom roles.", "unit": null, "limit": null, - "note": "Unlimited" + "note": null }, { - "key": "api_access", - "name": "API access", - "description": "Access your data via our developer-friendly API.", + "key": "advanced_permissions", + "name": "Advanced permissions", + "description": "Control who can access and modify data and features within your organization.", "unit": null, "limit": null, "note": null }, { - "key": "social_sso", - "name": "SSO via Google, Github, or Gitlab", - "description": "Log in to PostHog with your Google, Github, or Gitlab account.", + "key": "audit_logs", + "name": "Audit logs", + "description": "See who in your organization has accessed or modified entities within PostHog.", "unit": null, "limit": null, - "note": null + "note": "Advanced" }, { - "key": "project_based_permissioning", - "name": "Project permissions", - "description": "Restrict access to data within the organization to only those who need it.", + "key": "hipaa_baa", + "name": "HIPAA BAA", + "description": "Get a signed HIPAA Business Associate Agreement (BAA) to use PostHog in a HIPAA-compliant manner.", "unit": null, "limit": null, "note": null }, { - "key": "white_labelling", - "name": "White labeling", - "description": "Use your own branding in your PostHog organization.", + "key": "custom_msa", + "name": "Custom MSA", + "description": "Get a custom Master Services Agreement (MSA) to use PostHog in a way that fits your company's needs.", "unit": null, "limit": null, "note": null @@ -2108,28 +3517,28 @@ "note": null }, { - "key": "dedicated_support", - "name": "Slack (dedicated channel)", - "description": "Get help directly from our support team in a dedicated Slack channel shared between you and the PostHog team.", + "key": "email_support", + "name": "Email support", + "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", "unit": null, "limit": null, - "note": "$2k/month spend or above" + "note": null }, { - "key": "email_support", - "name": "Direct access to engineers", - "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", + "key": "dedicated_support", + "name": "Dedicated account manager", + "description": "Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.", "unit": null, "limit": null, "note": null }, { - "key": "terms_and_conditions", - "name": "Terms and conditions", - "description": "Terms and conditions", + "key": "priority_support", + "name": "Priority support", + "description": "Get help from our team faster than other customers.", "unit": null, "limit": null, - "note": "Standard" + "note": null }, { "key": "security_assessment", @@ -2138,11 +3547,29 @@ "unit": null, "limit": null, "note": null + }, + { + "key": "training", + "name": "Ongoing training", + "description": "Get training from our team to help you quickly get up and running with PostHog.", + "unit": null, + "limit": null, + "note": null + }, + { + "key": "configuration_support", + "name": "Personalized onboarding", + "description": "Get help from our team to create dashboards that will help you understand your data and your business.", + "unit": null, + "limit": null, + "note": null } ], "tiers": null, - "current_plan": true, - "included_if": "has_subscription" + "current_plan": false, + "included_if": null, + "contact_support": true, + "unit_amount_usd": null } ], "type": "platform_and_support", @@ -2160,8 +3587,250 @@ "projected_amount_usd": null, "unit": null, "addons": [], - "contact_support": true, - "inclusion_only": true + "contact_support": false, + "inclusion_only": true, + "features": [ + { + "key": "tracked_users", + "name": "Tracked users", + "description": "Track users across devices and sessions.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "team_members", + "name": "Team members", + "description": "PostHog doesn't charge per seat add your entire team!", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "api_access", + "name": "API access", + "description": "Access your data via our developer-friendly API.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "organizations_projects", + "name": "Projects", + "description": "Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "social_sso", + "name": "SSO via Google, Github, or Gitlab", + "description": "Log in to PostHog with your Google, Github, or Gitlab account.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "role_based_access", + "name": "Role-based access", + "description": "Control access to features like experiments, session recordings, and feature flags with custom roles.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "project_based_permissioning", + "name": "Project permissions", + "description": "Restrict access to data within the organization to only those who need it.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "advanced_permissions", + "name": "Advanced permissions", + "description": "Control who can access and modify data and features within your organization.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "saml", + "name": "SAML SSO", + "description": "Allow your organization's users to log in with SAML.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "sso_enforcement", + "name": "Enforce SSO login", + "description": "Users can only sign up and log in to your PostHog organization with your specified SSO provider.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "2fa", + "name": "2FA", + "description": "Secure your PostHog account with two-factor authentication.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "2fa_enforcement", + "name": "Enforce 2FA", + "description": "Require all users in your organization to enable two-factor authentication.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "white_labelling", + "name": "White labeling", + "description": "Use your own branding on surveys, shared dashboards, shared insights, and more.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "community_support", + "name": "Community support", + "description": "Get help from other users and PostHog team members in our Community forums.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "dedicated_support", + "name": "Dedicated account manager", + "description": "Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "email_support", + "name": "Email support", + "description": "Get help directly from our product engineers via email. No wading through multiple support people before you get help.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "priority_support", + "name": "Priority support", + "description": "Get help from our team faster than other customers.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "training", + "name": "Ongoing training", + "description": "Get training from our team to help you quickly get up and running with PostHog.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "configuration_support", + "name": "Personalized onboarding", + "description": "Get help from our team to create dashboards that will help you understand your data and your business.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "terms_and_conditions", + "name": "Terms and conditions", + "description": "Terms and conditions", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "security_assessment", + "name": "Security assessment", + "description": "Security assessment", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "bespoke_pricing", + "name": "Bespoke pricing", + "description": "Custom pricing to fit your company's needs.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "invoice_payments", + "name": "Payment via invoicing", + "description": "Pay for your PostHog subscription via invoice.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "support_slas", + "name": "Support SLAs", + "description": "Support SLAs", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "audit_logs", + "name": "Audit logs", + "description": "See who in your organization has accessed or modified entities within PostHog.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "hipaa_baa", + "name": "HIPAA BAA", + "description": "Get a signed HIPAA Business Associate Agreement (BAA) to use PostHog in a HIPAA-compliant manner.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "custom_msa", + "name": "Custom MSA", + "description": "Get a custom Master Services Agreement (MSA) to use PostHog in a way that fits your company's needs.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "team_collaboration", + "name": "Team collaboration features", + "description": "Work together better with tags on dashboards and insights; descriptions on insights, events, & properties; verified events; comments on almost anything.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "ingestion_taxonomy", + "name": "Ingestion taxonomy", + "description": "Mark events as verified or unverified to help you understand the quality of your data.", + "images": null, + "icon_key": null, + "type": null + }, + { + "key": "tagging", + "name": "Dashboard tags", + "description": "Organize dashboards with tags.", + "images": null, + "icon_key": null, + "type": null + } + ] } ], "custom_limits_usd": {}, @@ -2172,7 +3841,7 @@ }, "recordings": { "usage": 0, - "limit": 15000 + "limit": 5000 }, "feature_flag_requests": { "usage": 0, @@ -2181,10 +3850,6 @@ "survey_responses": { "usage": 0, "limit": 250 - }, - "synced_rows": { - "usage": 0, - "limit": 0 } }, "free_trial_until": null, @@ -2192,5 +3857,11 @@ "discount_amount_usd": null, "amount_off_expires_at": null, "never_drop_data": null, - "stripe_portal_url": "https://billing.stripe.com/p/session/test_YWNjdF8xSElNRERFdUlhdFJYU2R6LF9PdXdxeDNqcktEWWdEM1FhalNRNmNCdTZCaUJsVVBi01006f6sniQg" + "customer_trust_scores": { + "surveys": 0, + "feature_flags": 0, + "session_replay": 3, + "product_analytics": 3 + }, + "stripe_portal_url": "https://billing.stripe.com/p/session/test_YWNjdF8xSElNRERFdUlhdFJYU2R6LF9QaEVpVHFJNXdKYk9DaG04SVhMaUV4TDlxOTR1WEZi0100SMJCDr2e" } diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr index 3c6b8ef78c385..54b67fa7d359b 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results ''' - /* user_id:107 celery:posthog.tasks.tasks.sync_insight_caching_state */ + /* user_id:108 celery:posthog.tasks.tasks.sync_insight_caching_state */ SELECT team_id, date_diff('second', max(timestamp), now()) AS age FROM events diff --git a/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--dark.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--dark.png index e0f2ce83d97d5..7c9894110c558 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--dark.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--light.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--light.png index f0eb7967c5b1e..8e15b103248ed 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--light.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--long-loading--light.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png index f3521aa3d9ba9..b760d16fd5592 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png index a0bf0be976004..f23ba81faf042 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png differ diff --git a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png index 243cee06e1f0d..593c08ed4bcec 100644 Binary files a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png and b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--light.png b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--light.png index dfc8dab84102a..a4a4045de7b70 100644 Binary files a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--light.png and b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png index ae4c62ad0824c..ab33e8563f5f7 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png b/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png index b0aae8a4b0b27..08a1cffcd03ae 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png differ diff --git a/frontend/src/layout/GlobalModals.tsx b/frontend/src/layout/GlobalModals.tsx index 6340f68af9823..7ef5f0d546afb 100644 --- a/frontend/src/layout/GlobalModals.tsx +++ b/frontend/src/layout/GlobalModals.tsx @@ -2,6 +2,7 @@ import { LemonModal } from '@posthog/lemon-ui' import { actions, kea, path, reducers, useActions, useValues } from 'kea' import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { HedgehogBuddyWithLogic } from 'lib/components/HedgehogBuddy/HedgehogBuddyWithLogic' +import { UpgradeModal } from 'lib/components/UpgradeModal/UpgradeModal' import { Prompt } from 'lib/logic/newPrompt/Prompt' import { Setup2FA } from 'scenes/authentication/Setup2FA' import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal' @@ -9,7 +10,6 @@ import { membersLogic } from 'scenes/organization/membersLogic' import { CreateProjectModal } from 'scenes/project/CreateProjectModal' import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { InviteModal } from 'scenes/settings/organization/InviteModal' -import { UpgradeModal } from 'scenes/UpgradeModal' import { userLogic } from 'scenes/userLogic' import type { globalModalsLogicType } from './GlobalModalsType' diff --git a/frontend/src/layout/navigation/OrganizationSwitcher.tsx b/frontend/src/layout/navigation/OrganizationSwitcher.tsx index 850f8642669c2..692d0c8846ad1 100644 --- a/frontend/src/layout/navigation/OrganizationSwitcher.tsx +++ b/frontend/src/layout/navigation/OrganizationSwitcher.tsx @@ -1,5 +1,6 @@ import { IconPlus } from '@posthog/icons' import { useActions, useValues } from 'kea' +import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' @@ -7,7 +8,6 @@ import { Lettermark } from 'lib/lemon-ui/Lettermark' import { membershipLevelToName } from 'lib/utils/permissioning' import { organizationLogic } from 'scenes/organizationLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { sceneLogic } from 'scenes/sceneLogic' import { userLogic } from 'scenes/userLogic' import { AvailableFeature, OrganizationBasicType } from '~/types' @@ -48,7 +48,7 @@ export function OtherOrganizationButton({ export function NewOrganizationButton(): JSX.Element { const { closeAccountPopover } = useActions(navigationLogic) const { showCreateOrganizationModal } = useActions(globalModalsLogic) - const { guardAvailableFeature } = useActions(sceneLogic) + const { guardAvailableFeature } = useValues(upgradeModalLogic) return ( void }): JSX.Element { const { currentOrganization, projectCreationForbiddenReason } = useValues(organizationLogic) const { currentTeam } = useValues(teamLogic) - const { guardAvailableFeature } = useActions(sceneLogic) + const { guardAvailableFeature } = useValues(upgradeModalLogic) const { showCreateProjectModal } = useActions(globalModalsLogic) return ( @@ -46,14 +46,12 @@ export function ProjectSwitcherOverlay({ onClickInside }: { onClickInside?: () = fullWidth disabled={!!projectCreationForbiddenReason} tooltip={projectCreationForbiddenReason} + data-attr="new-project-button" onClick={() => { onClickInside?.() - guardAvailableFeature( - AvailableFeature.ORGANIZATIONS_PROJECTS, - showCreateProjectModal, - undefined, - currentOrganization?.teams?.length - ) + guardAvailableFeature(AvailableFeature.ORGANIZATIONS_PROJECTS, showCreateProjectModal, { + currentUsage: currentOrganization?.teams?.length, + }) }} > New project diff --git a/frontend/src/lib/components/BillingUpgradeCTA.tsx b/frontend/src/lib/components/BillingUpgradeCTA.tsx new file mode 100644 index 0000000000000..cf9278e909af3 --- /dev/null +++ b/frontend/src/lib/components/BillingUpgradeCTA.tsx @@ -0,0 +1,13 @@ +import { useActions } from 'kea' +import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { useEffect } from 'react' + +export function BillingUpgradeCTA({ children, ...props }: LemonButtonProps): JSX.Element { + const { reportBillingCTAShown } = useActions(eventUsageLogic) + useEffect(() => { + reportBillingCTAShown() + }, []) + + return {children} +} diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index 371e8c3a12524..e29c933aac59f 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -1,5 +1,4 @@ import { - IconApps, IconCalculator, IconChat, IconCheck, @@ -7,6 +6,7 @@ import { IconDashboard, IconDatabase, IconDay, + IconDecisionTree, IconExternal, IconEye, IconFunnels, @@ -617,7 +617,7 @@ export const commandPaletteLogic = kea([ }, }, { - icon: IconApps, + icon: IconDecisionTree, display: 'Go to Apps', synonyms: ['integrations'], executor: () => { diff --git a/frontend/src/lib/components/ObjectTags/ObjectTags.tsx b/frontend/src/lib/components/ObjectTags/ObjectTags.tsx index aa4ca58253c96..b58b4a2caa33e 100644 --- a/frontend/src/lib/components/ObjectTags/ObjectTags.tsx +++ b/frontend/src/lib/components/ObjectTags/ObjectTags.tsx @@ -9,11 +9,11 @@ import { objectTagsLogic } from 'lib/components/ObjectTags/objectTagsLogic' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { colorForString } from 'lib/utils' import { CSSProperties, useMemo } from 'react' -import { sceneLogic } from 'scenes/sceneLogic' import { AvailableFeature } from '~/types' import { SelectGradientOverflow } from '../SelectGradientOverflow' +import { upgradeModalLogic } from '../UpgradeModal/upgradeModalLogic' interface ObjectTagsPropsBase { tags: string[] @@ -61,7 +61,7 @@ export function ObjectTags({ }: ObjectTagsProps): JSX.Element { const objectTagId = useMemo(() => uniqueMemoizedIndex++, []) const logic = objectTagsLogic({ id: objectTagId, onChange, tags }) - const { guardAvailableFeature } = useActions(sceneLogic) + const { guardAvailableFeature } = useValues(upgradeModalLogic) const { addingNewTag, cleanedNewTag, deletedTags } = useValues(logic) const { setAddingNewTag, setNewTag, handleDelete, handleAdd } = useActions(logic) diff --git a/frontend/src/lib/components/PayGateMini/PayGateMini.tsx b/frontend/src/lib/components/PayGateMini/PayGateMini.tsx index 38acd75ace822..5ea5ec55e8d0a 100644 --- a/frontend/src/lib/components/PayGateMini/PayGateMini.tsx +++ b/frontend/src/lib/components/PayGateMini/PayGateMini.tsx @@ -10,10 +10,10 @@ import { useEffect } from 'react' import { billingLogic } from 'scenes/billing/billingLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { getProductIcon } from 'scenes/products/Products' -import { sceneLogic } from 'scenes/sceneLogic' import { AvailableFeature } from '~/types' +import { upgradeModalLogic } from '../UpgradeModal/upgradeModalLogic' import { PayGateMiniButton } from './PayGateMiniButton' import { payGateMiniLogic } from './payGateMiniLogic' @@ -47,7 +47,7 @@ export function PayGateMini({ const { preflight } = useValues(preflightLogic) const { billing, billingLoading } = useValues(billingLogic) const { featureFlags } = useValues(featureFlagLogic) - const { hideUpgradeModal } = useActions(sceneLogic) + const { hideUpgradeModal } = useActions(upgradeModalLogic) useEffect(() => { if (gateVariant) { diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx index 8f6cb1e96a68b..673bd426629bf 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx @@ -129,7 +129,7 @@ export function PropertyValue({ value={formattedValues} mode={isMultiSelect ? 'multiple' : 'single'} allowCustomValues - onChange={(nextVal) => setValue(nextVal)} + onChange={(nextVal) => (isMultiSelect ? setValue(nextVal) : setValue(nextVal[0]))} onInputChange={onSearchTextChange} placeholder={placeholder} options={displayOptions.map(({ name: _name }, index) => { diff --git a/frontend/src/lib/components/Sharing/SharingModal.tsx b/frontend/src/lib/components/Sharing/SharingModal.tsx index 410af5493ef16..8db90e2bca2f2 100644 --- a/frontend/src/lib/components/Sharing/SharingModal.tsx +++ b/frontend/src/lib/components/Sharing/SharingModal.tsx @@ -15,10 +15,10 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip' import { copyToClipboard } from 'lib/utils/copyToClipboard' import { useEffect, useState } from 'react' import { DashboardCollaboration } from 'scenes/dashboard/DashboardCollaborators' -import { sceneLogic } from 'scenes/sceneLogic' import { AvailableFeature, InsightModel, InsightShortId, InsightType } from '~/types' +import { upgradeModalLogic } from '../UpgradeModal/upgradeModalLogic' import { sharingLogic } from './sharingLogic' export const SHARING_MODAL_WIDTH = 600 @@ -64,7 +64,7 @@ export function SharingModalContent({ shareLink, } = useValues(sharingLogic(logicProps)) const { setIsEnabled, togglePreview } = useActions(sharingLogic(logicProps)) - const { guardAvailableFeature } = useActions(sceneLogic) + const { guardAvailableFeature } = useValues(upgradeModalLogic) const [iframeLoaded, setIframeLoaded] = useState(false) diff --git a/frontend/src/scenes/UpgradeModal.tsx b/frontend/src/lib/components/UpgradeModal/UpgradeModal.tsx similarity index 79% rename from frontend/src/scenes/UpgradeModal.tsx rename to frontend/src/lib/components/UpgradeModal/UpgradeModal.tsx index 0a3c7bd625800..d731a6774772b 100644 --- a/frontend/src/scenes/UpgradeModal.tsx +++ b/frontend/src/lib/components/UpgradeModal/UpgradeModal.tsx @@ -2,11 +2,12 @@ import { LemonModal } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' -import { sceneLogic } from './sceneLogic' +import { upgradeModalLogic } from './upgradeModalLogic' export function UpgradeModal(): JSX.Element { - const { upgradeModalFeatureKey, upgradeModalFeatureUsage, upgradeModalIsGrandfathered } = useValues(sceneLogic) - const { hideUpgradeModal } = useActions(sceneLogic) + const { upgradeModalFeatureKey, upgradeModalFeatureUsage, upgradeModalIsGrandfathered } = + useValues(upgradeModalLogic) + const { hideUpgradeModal } = useActions(upgradeModalLogic) return upgradeModalFeatureKey ? ( @@ -17,10 +18,10 @@ export function UpgradeModal(): JSX.Element { isGrandfathered={upgradeModalIsGrandfathered ?? undefined} background={false} > - <> +
You should have access to this feature already. If you are still seeing this modal, please let us know 🙂 - +
diff --git a/frontend/src/lib/components/UpgradeModal/upgradeModalLogic.ts b/frontend/src/lib/components/UpgradeModal/upgradeModalLogic.ts new file mode 100644 index 0000000000000..9542ac6a208dc --- /dev/null +++ b/frontend/src/lib/components/UpgradeModal/upgradeModalLogic.ts @@ -0,0 +1,96 @@ +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { userLogic } from 'scenes/userLogic' + +import { AvailableFeature } from '~/types' + +import type { upgradeModalLogicType } from './upgradeModalLogicType' + +export type GuardAvailableFeatureFn = ( + featureKey: AvailableFeature, + featureAvailableCallback?: () => void, + options?: { + guardOnCloud?: boolean + guardOnSelfHosted?: boolean + currentUsage?: number + isGrandfathered?: boolean + } +) => boolean + +export const upgradeModalLogic = kea([ + path(['lib', 'components', 'UpgradeModal', 'upgradeModalLogic']), + connect(() => ({ + values: [preflightLogic, ['preflight'], featureFlagLogic, ['featureFlags'], userLogic, ['hasAvailableFeature']], + })), + actions({ + showUpgradeModal: (featureKey: AvailableFeature, currentUsage?: number, isGrandfathered?: boolean) => ({ + featureKey, + currentUsage, + isGrandfathered, + }), + hideUpgradeModal: true, + }), + reducers({ + upgradeModalFeatureKey: [ + null as AvailableFeature | null, + { + showUpgradeModal: (_, { featureKey }) => featureKey, + hideUpgradeModal: () => null, + }, + ], + upgradeModalFeatureUsage: [ + null as number | null, + { + showUpgradeModal: (_, { currentUsage }) => currentUsage ?? null, + hideUpgradeModal: () => null, + }, + ], + upgradeModalIsGrandfathered: [ + null as boolean | null, + { + showUpgradeModal: (_, { isGrandfathered }) => isGrandfathered ?? null, + hideUpgradeModal: () => null, + }, + ], + }), + selectors(({ actions }) => ({ + guardAvailableFeature: [ + (s) => [s.preflight, s.hasAvailableFeature], + (preflight, hasAvailableFeature): GuardAvailableFeatureFn => { + return (featureKey, featureAvailableCallback, options): boolean => { + const { + guardOnCloud = true, + guardOnSelfHosted = true, + currentUsage, + isGrandfathered, + } = options || {} + let featureAvailable: boolean + if (!preflight) { + featureAvailable = false + } else if (!guardOnCloud && preflight.cloud) { + featureAvailable = true + } else if (!guardOnSelfHosted && !preflight.cloud) { + featureAvailable = true + } else { + featureAvailable = hasAvailableFeature(featureKey, currentUsage) + } + + if (!featureAvailable) { + actions.showUpgradeModal(featureKey, currentUsage, isGrandfathered) + } else { + featureAvailableCallback?.() + } + + return featureAvailable + } + }, + ], + })), + listeners(() => ({ + showUpgradeModal: ({ featureKey }) => { + eventUsageLogic.actions.reportUpgradeModalShown(featureKey) + }, + })), +]) diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 77d5f9c37eb0f..522d393f0f9a6 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -202,6 +202,7 @@ export const FEATURE_FLAGS = { REPLAY_SIMILAR_RECORDINGS: 'session-replay-similar-recordings', // owner: #team-replay SAVED_NOT_PINNED: 'saved-not-pinned', // owner: #team-replay EXPORTS_SIDEPANEL: 'exports-sidepanel', // owner: #team-product-analytics + BILLING_UPGRADE_LANGUAGE: 'billing-upgrade-language', // owner: @biancayang NEW_EXPERIMENTS_UI: 'new-experiments-ui', // owner: @jurajmajerik #team-feature-success SESSION_REPLAY_V3_INGESTION_PLAYBACK: 'session-replay-v3-ingestion-playback', // owner: @benjackwhite SESSION_REPLAY_FILTER_ORDERING: 'session-replay-filter-ordering', // owner: #team-replay diff --git a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx index f3e39c46f1e11..967f18e323753 100644 --- a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx +++ b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx @@ -157,7 +157,7 @@ export function LemonInputSelect({ } } else if (e.key === 'ArrowDown') { e.preventDefault() - setSelectedIndex(Math.min(selectedIndex + 1, options.length - 1)) + setSelectedIndex(Math.min(selectedIndex + 1, visibleOptions.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setSelectedIndex(Math.max(selectedIndex - 1, 0)) diff --git a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx index a4dd176d9511b..3a50ea89355f8 100644 --- a/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx +++ b/frontend/src/lib/lemon-ui/LemonModal/LemonModal.tsx @@ -45,6 +45,7 @@ export interface LemonModalProps { forceAbovePopovers?: boolean contentRef?: React.RefCallback overlayRef?: React.RefCallback + 'data-attr'?: string } export const LemonModalHeader = ({ children, className }: LemonModalInnerProps): JSX.Element => { @@ -82,6 +83,7 @@ export function LemonModal({ contentRef, overlayRef, hideCloseButton = false, + 'data-attr': dataAttr, }: LemonModalProps): JSX.Element { const nodeRef = useRef(null) const [ignoredOverlayClickCount, setIgnoredOverlayClickCount] = useState(0) @@ -89,7 +91,7 @@ export function LemonModal({ useEffect(() => setIgnoredOverlayClickCount(0), [hasUnsavedInput]) // Reset when there no longer is unsaved input const modalContent = ( -
+
{closable && !hideCloseButton && ( // The key causes the div to be re-rendered, which restarts the animation, // providing immediate visual feedback on click diff --git a/frontend/src/lib/lemon-ui/icons/categories.ts b/frontend/src/lib/lemon-ui/icons/categories.ts new file mode 100644 index 0000000000000..f209c9f400900 --- /dev/null +++ b/frontend/src/lib/lemon-ui/icons/categories.ts @@ -0,0 +1,189 @@ +export const UNUSED_ICONS = [ + 'IconAdvanced', + 'IconAsterisk', + 'IconGridMasonry', + 'IconApps', + 'IconRibbon', + 'IconPulse', + 'IconPineapple', + 'IconPizza', + 'IconTarget', + 'IconThumbsUp', + 'IconThumbsUpFilled', + 'IconThumbsDown', + 'IconThumbsDownFilled', + 'IconShieldLock', +] + +export const OBJECTS = { + Misc: [ + 'IconPalette', + 'IconMegaphone', + 'IconRocket', + 'IconMap', + 'IconTie', + 'IconCoffee', + 'IconFlag', + 'IconCreditCard', + 'IconCrown', + 'IconBolt', + 'IconBook', + 'IconConfetti', + 'IconPresent', + 'IconMagicWand', + 'IconMagic', + 'IconHelmet', + 'IconSpotlight', + 'IconGraduationCap', + 'IconLightBulb', + 'IconBell', + 'IconBox', + 'IconBuilding', + 'IconEye', + 'IconFeatures', + 'IconHome', + 'IconHomeFilled', + 'IconGear', + 'IconGearFilled', + 'IconStack', + ], + People: ['IconPeople', 'IconPeopleFilled', 'IconPerson', 'IconProfile', 'IconUser'], + 'Business & Finance': ['IconStore', 'IconCart', 'IconReceipt', 'IconPiggyBank'], + Time: ['IconHourglass', 'IconCalendar', 'IconClock'], + Nature: ['IconDay', 'IconNight', 'IconGlobe', 'IconCloud', 'IconBug'], + Text: ['IconDocument', 'IconBrackets', 'IconTextWidth', 'IconQuote', 'IconLetter', 'IconNewspaper'], +} + +export const TECHNOLOGY = { + Messaging: ['IconSend', 'IconHeadset', 'IconMessage', 'IconNotification', 'IconChat', 'IconThoughtBubble'], + Hardware: [ + 'IconLaptop', + 'IconPhone', + 'IconWebcam', + 'IconMicrophone', + 'IconKeyboard', + 'IconServer', + 'IconDatabase', + 'IconHardDrive', + ], + Software: ['IconBrowser', 'IconCode', 'IconCodeInsert', 'IconTerminal', 'IconApp'], + UI: [ + 'IconPassword', + 'IconToggle', + 'IconLoading', + 'IconSpinner', + 'IconBrightness', + 'IconCursor', + 'IconCursorBox', + 'IconCursorClick', + 'IconToolbar', + 'IconToolbarFilled', + 'IconCheckbox', + 'IconList', + 'IconColumns', + ], +} + +export const ELEMENTS = { + Actions: [ + 'IconCopy', + 'IconTrash', + 'IconUndo', + 'IconRedo', + 'IconRevert', + 'IconSearch', + 'IconUpload', + 'IconShare', + 'IconDownload', + 'IconLeave', + 'IconPin', + 'IconPinFilled', + 'IconPencil', + 'IconOpenSidebar', + 'IconFilter', + 'IconArchive', + 'IconSort', + 'IconExternal', + ], + Symbols: [ + 'IconLock', + 'IconUnlock', + 'IconPrivacy', + 'IconShield', + 'IconWarning', + 'IconQuestion', + 'IconInfo', + 'IconCheckCircle', + 'IconCheck', + 'IconX', + 'IconEllipsis', + ], + 'Arrows & Shapes': [ + 'IconArrowLeft', + 'IconArrowRight', + 'IconArrowCircleLeft', + 'IconArrowCircleRight', + 'IconArrowRightDown', + 'IconArrowUpRight', + 'IconCollapse', + 'IconExpand', + 'IconCollapse45', + 'IconExpand45', + 'IconChevronDown', + 'IconTriangleDown', + 'IconTriangleDownFilled', + 'IconTriangleUp', + 'IconTriangleUpFilled', + 'IconStar', + 'IconStarFilled', + 'IconHeart', + 'IconHeartFilled', + ], + Mathematics: [ + 'IconPlus', + 'IconPlusSmall', + 'IconPlusSquare', + 'IconMinus', + 'IconMinusSmall', + 'IconMinusSquare', + 'IconMultiply', + 'IconPercentage', + 'IconCalculator', + ], +} + +export const TEAMS_AND_COMPANIES = { + Analytics: [ + 'IconCorrelationAnalysis', + 'IconGraph', + 'IconLineGraph', + 'IconRetention', + 'IconFunnels', + 'IconGanttChart', + 'IconTrending', + 'IconTrends', + 'IconLifecycle', + 'IconPieChart', + 'IconUserPaths', + 'IconStickiness', + 'IconPageChart', + 'IconSampling', + 'IconLive', + 'IconBadge', + ], + Replay: [ + 'IconPlay', + 'IconPlayFilled', + 'IconPlaylist', + 'IconPause', + 'IconPauseFilled', + 'IconRewind', + 'IconRecord', + 'IconRewindPlay', + 'IconVideoCamera', + ], + 'Feature Success': ['IconFlask', 'IconTestTube', 'IconMultivariateTesting', 'IconSplitTesting'], + Pipeline: ['IconWebhooks', 'IconDecisionTree'], + 'Product OS': ['IconNotebook', 'IconHogQL', 'IconDashboard', 'IconSupport'], + Logos: ['IconLogomark', 'IconGithub'], +} diff --git a/frontend/src/lib/lemon-ui/icons/icons.test.ts b/frontend/src/lib/lemon-ui/icons/icons.test.ts new file mode 100644 index 0000000000000..f3b888ca4ac80 --- /dev/null +++ b/frontend/src/lib/lemon-ui/icons/icons.test.ts @@ -0,0 +1,17 @@ +import * as packageIcons from '@posthog/icons' + +import { ELEMENTS, OBJECTS, TEAMS_AND_COMPANIES, TECHNOLOGY, UNUSED_ICONS } from './categories' + +describe('icons', () => { + it('ensures all icons are categorised', async () => { + const validPackageIcons = Object.keys(packageIcons).filter((i) => !['BaseIcon', 'default'].includes(i)) + const categories = { ...OBJECTS, ...TECHNOLOGY, ...ELEMENTS, ...TEAMS_AND_COMPANIES } + const categorisedIcons = Object.values(categories) + .map((category) => Object.values(category)) + .flat(2) + + const allIcons = [...categorisedIcons, ...UNUSED_ICONS] + + expect(validPackageIcons.filter((i) => !allIcons.includes(i))).toEqual([]) + }) +}) diff --git a/frontend/src/lib/lemon-ui/icons/icons3000.stories.tsx b/frontend/src/lib/lemon-ui/icons/icons3000.stories.tsx new file mode 100644 index 0000000000000..69af4d9765ef1 --- /dev/null +++ b/frontend/src/lib/lemon-ui/icons/icons3000.stories.tsx @@ -0,0 +1,92 @@ +import * as packageIcons from '@posthog/icons' +import { Meta, StoryObj } from '@storybook/react' +import { copyToClipboard } from 'lib/utils/copyToClipboard' + +import { LemonCollapse } from '../LemonCollapse' +import { Tooltip } from '../Tooltip' +import { ELEMENTS, OBJECTS, TEAMS_AND_COMPANIES, TECHNOLOGY } from './categories' + +const meta: Meta = { + title: 'PostHog 3000/Icons', + tags: ['test-skip'], + parameters: { + previewTabs: { + 'storybook/docs/panel': { + hidden: true, + }, + }, + }, +} +export default meta + +const posthogIcons = Object.entries(packageIcons) + .filter(([key]) => key !== 'BaseIcon') + .map(([key, Icon]) => ({ name: key, icon: Icon })) + +const IconTemplate = ({ icons }: { icons: { name: string; icon: any }[] }): JSX.Element => { + const onClick = (name: string): void => { + void copyToClipboard(name) + } + + return ( +
+ {icons.map(({ name, icon: Icon }) => { + return ( +
onClick(name)} key={name} className="flex justify-center"> + +
+ + {name} +
+
+
+ ) + })} +
+ ) +} + +export function Alphabetical(): JSX.Element { + return +} + +const GroupBase = ({ group }: { group: Record }): JSX.Element => { + return ( + { + return { + key, + header: key, + content: ( + { + return { name: icon, icon: packageIcons[icon] } + })} + /> + ), + } + })} + /> + ) +} + +export const Elements: StoryObj = (): JSX.Element => { + return +} +Elements.storyName = 'Category - Elements' + +export const TeamsAndCompanies: StoryObj = (): JSX.Element => { + return +} +TeamsAndCompanies.storyName = 'Category - Teams & Companies' + +export const Technology: StoryObj = (): JSX.Element => { + return +} +Technology.storyName = 'Category - Technology' + +export const Objects: StoryObj = (): JSX.Element => { + return +} +Objects.storyName = 'Category - Objects' diff --git a/frontend/src/lib/utils/eventUsageLogic.ts b/frontend/src/lib/utils/eventUsageLogic.ts index 555f99cb715a1..e88617ddd7bad 100644 --- a/frontend/src/lib/utils/eventUsageLogic.ts +++ b/frontend/src/lib/utils/eventUsageLogic.ts @@ -507,8 +507,12 @@ export const eventUsageLogic = kea([ reportCommandBarSearchResultOpened: (type: ResultType) => ({ type }), reportCommandBarActionSearch: (query: string) => ({ query }), reportCommandBarActionResultExecuted: (resultDisplay) => ({ resultDisplay }), + reportBillingCTAShown: true, }), listeners(({ values }) => ({ + reportBillingCTAShown: () => { + posthog.capture('billing CTA shown') + }, reportAxisUnitsChanged: (properties) => { posthog.capture('axis units changed', properties) }, diff --git a/frontend/src/mocks/fixtures/_billing_v2.tsx b/frontend/src/mocks/fixtures/_billing_v2.tsx index 017c2ed16bc6b..43b499844eb70 100644 --- a/frontend/src/mocks/fixtures/_billing_v2.tsx +++ b/frontend/src/mocks/fixtures/_billing_v2.tsx @@ -3,12 +3,12 @@ import { dayjs } from 'lib/dayjs' import { BillingV2Type } from '~/types' export const billingJson: BillingV2Type = { - customer_id: 'cus_PRQtW3VM1Kiw7e', + customer_id: 'cus_Pg7PIL8MsKi6bx', deactivated: false, has_active_subscription: true, billing_period: { - current_period_start: dayjs('2023-05-01T23:59:59Z'), - current_period_end: dayjs('2023-06-01T23:59:59Z'), + current_period_start: dayjs('2024-03-07T22:54:32Z'), + current_period_end: dayjs('2024-04-07T22:54:32Z'), interval: 'month', }, current_total_amount_usd: '403.07', @@ -16,12 +16,13 @@ export const billingJson: BillingV2Type = { products: [ { name: 'Product analytics', - headline: null, - description: 'Trends, funnels, path analysis, CDP + more.', + headline: 'Product analytics with autocapture', + description: + 'A comprehensive product analytics platform built to natively work with session replay, feature flags, A/B testing, and surveys.', price_description: null, usage_key: 'events', image_url: 'https://posthog.com/images/products/product-analytics/product-analytics.png', - screenshot_url: null, + screenshot_url: 'https://posthog.com/images/products/product-analytics/screenshot-product-analytics.png', icon_key: 'IconGraph', docs_url: 'https://posthog.com/docs/product-analytics', subscribed: true, @@ -29,9 +30,10 @@ export const billingJson: BillingV2Type = { { plan_key: 'free-20230117', product_key: 'product_analytics', - name: 'Product analytics', - description: 'Trends, funnels, path analysis, CDP + more.', - image_url: 'https://posthog.com/images/product/product-icons/product-analytics.svg', + name: 'Free', + description: + 'A comprehensive product analytics platform built to natively work with session replay, feature flags, A/B testing, and surveys.', + image_url: 'https://posthog.com/images/products/product-analytics/product-analytics.png', docs_url: 'https://posthog.com/docs/product-analytics', note: null, unit: 'event', @@ -49,7 +51,8 @@ export const billingJson: BillingV2Type = { { key: 'funnels', name: 'Funnels', - description: 'Visualize user dropoff between a sequence of events.', + description: + 'Visualize user dropoff between a sequence of events. See conversion rate over time, use flexible step ordering, set exclusion steps, and more.', unit: null, limit: null, note: null, @@ -64,7 +67,7 @@ export const billingJson: BillingV2Type = { }, { key: 'paths', - name: 'Paths', + name: 'User paths', description: 'Limited paths excludes: customizing path insights by setting the maximum number of paths, number of people on each path, how path names appear', unit: null, @@ -83,13 +86,16 @@ export const billingJson: BillingV2Type = { tiers: null, current_plan: false, included_if: null, + contact_support: null, + unit_amount_usd: null, }, { plan_key: 'paid-20240111', product_key: 'product_analytics', - name: 'Product analytics', - description: 'Trends, funnels, path analysis, CDP + more.', - image_url: 'https://posthog.com/images/product/product-icons/product-analytics.svg', + name: 'Paid', + description: + 'A comprehensive product analytics platform built to natively work with session replay, feature flags, A/B testing, and surveys.', + image_url: 'https://posthog.com/images/products/product-analytics/product-analytics.png', docs_url: 'https://posthog.com/docs/product-analytics', note: null, unit: 'event', @@ -107,7 +113,8 @@ export const billingJson: BillingV2Type = { { key: 'funnels', name: 'Funnels', - description: 'Visualize user dropoff between a sequence of events.', + description: + 'Visualize user dropoff between a sequence of events. See conversion rate over time, use flexible step ordering, set exclusion steps, and more.', unit: null, limit: null, note: null, @@ -122,7 +129,7 @@ export const billingJson: BillingV2Type = { }, { key: 'paths', - name: 'Paths', + name: 'User paths', description: 'Limited paths excludes: customizing path insights by setting the maximum number of paths, number of people on each path, how path names appear', unit: null, @@ -147,32 +154,6 @@ export const billingJson: BillingV2Type = { limit: null, note: null, }, - { - key: 'advanced_permissions', - name: 'Dashboard permissions', - description: - 'Restrict access to dashboards within the organization to only those who need it.', - unit: null, - limit: null, - note: null, - }, - { - key: 'team_collaboration', - name: 'Tags & text cards', - description: - 'Keep organized by adding tags to your dashboards, cohorts and more. Add text cards and descriptions to your dashboards to provide context to your team.', - unit: null, - limit: null, - note: null, - }, - { - key: 'ingestion_taxonomy', - name: 'Ingestion taxonomy', - description: 'Ingestion taxonomy', - unit: null, - limit: null, - note: null, - }, { key: 'correlation_analysis', name: 'Correlation analysis', @@ -182,19 +163,11 @@ export const billingJson: BillingV2Type = { limit: null, note: null, }, - { - key: 'tagging', - name: 'Dashboard tags', - description: 'Organize dashboards with tags.', - unit: null, - limit: null, - note: null, - }, { key: 'behavioral_cohort_filtering', - name: 'Lifecycle cohorts', + name: 'Lifecycle', description: - 'Group users based on their long term behavior, such as whether they frequently performed an event, or have recently stopped performing an event.', + 'Discover how your active users break down, highlighting those who have recently stopped being active or those who have just become active for the first time.', unit: null, limit: null, note: null, @@ -275,6 +248,8 @@ export const billingJson: BillingV2Type = { ], current_plan: true, included_if: null, + contact_support: null, + unit_amount_usd: null, }, ], type: 'product_analytics', @@ -349,7 +324,7 @@ export const billingJson: BillingV2Type = { current_amount_usd_before_addons: '0.00', current_amount_usd: '0.00', current_usage: 882128, - usage_limit: 3624548, + usage_limit: 882128, has_exceeded_limit: false, percentage_usage: 0.4423939206, projected_usage: 7000000, @@ -443,7 +418,7 @@ export const billingJson: BillingV2Type = { { plan_key: 'addon-20230509', product_key: 'group_analytics', - name: 'Group analytics', + name: 'Addon', description: 'Associate events with a group or entity - such as a company, community, or project. Analyze these events as if they were sent by that entity itself. Great for B2B, marketplaces, and more.', image_url: 'https://posthog.com/images/product/product-icons/group-analytics.svg', @@ -529,6 +504,8 @@ export const billingJson: BillingV2Type = { ], current_plan: false, included_if: null, + contact_support: null, + unit_amount_usd: null, }, ], contact_support: false, @@ -620,7 +597,7 @@ export const billingJson: BillingV2Type = { { plan_key: 'addon-20240111', product_key: 'data_pipelines', - name: 'Data pipelines', + name: 'Addon', description: 'Get your PostHog data into your data warehouse or other tools like BigQuery, Redshift, Customer.io, and more.', image_url: null, @@ -706,6 +683,8 @@ export const billingJson: BillingV2Type = { ], current_plan: false, included_if: null, + contact_support: null, + unit_amount_usd: null, }, ], contact_support: false, @@ -780,31 +759,6 @@ export const billingJson: BillingV2Type = { icon_key: 'IconNotification', type: 'secondary', }, - { - key: 'team_collaboration', - name: 'Tags & text cards', - description: - 'Keep organized by adding tags to your dashboards, cohorts and more. Add text cards and descriptions to your dashboards to provide context to your team.', - images: null, - icon_key: null, - type: null, - }, - { - key: 'advanced_permissions', - name: 'Dashboard permissions', - description: 'Restrict access to dashboards within the organization to only those who need it.', - images: null, - icon_key: null, - type: null, - }, - { - key: 'ingestion_taxonomy', - name: 'Ingestion taxonomy', - description: 'Ingestion taxonomy', - images: null, - icon_key: null, - type: null, - }, { key: 'paths_advanced', name: 'Advanced paths', @@ -826,14 +780,6 @@ export const billingJson: BillingV2Type = { icon_key: null, type: 'primary', }, - { - key: 'tagging', - name: 'Dashboard tags', - description: 'Organize dashboards with tags.', - images: null, - icon_key: null, - type: null, - }, { key: 'behavioral_cohort_filtering', name: 'Lifecycle', @@ -899,13 +845,13 @@ export const billingJson: BillingV2Type = { }, { name: 'Session replay', - headline: null, + headline: 'Watch how users experience your app', description: - 'Searchable recordings of people using your app or website with console logs and behavioral bucketing.', + 'Session replay helps you diagnose issues and understand user behavior in your product or website.', price_description: null, usage_key: 'recordings', - image_url: 'https://posthog.com/images/product/product-icons/session-replay.svg', - screenshot_url: null, + image_url: 'https://posthog.com/images/products/session-replay/session-replay.png', + screenshot_url: 'https://posthog.com/images/products/session-replay/screenshot-session-replay.png', icon_key: 'IconRewindPlay', docs_url: 'https://posthog.com/docs/session-replay', subscribed: true, @@ -913,10 +859,10 @@ export const billingJson: BillingV2Type = { { plan_key: 'free-20231218', product_key: 'session_replay', - name: 'Session replay', + name: 'Free', description: - 'Searchable recordings of people using your app or website with console logs and behavioral bucketing.', - image_url: 'https://posthog.com/images/product/product-icons/session-replay.svg', + 'Session replay helps you diagnose issues and understand user behavior in your product or website.', + image_url: 'https://posthog.com/images/products/session-replay/session-replay.png', docs_url: 'https://posthog.com/docs/session-replay', note: null, unit: 'recording', @@ -925,7 +871,7 @@ export const billingJson: BillingV2Type = { { key: 'console_logs', name: 'Console logs', - description: "Diagnose issues by inspecting errors in the user's network console", + description: "Debug issues faster by browsing the user's console.", unit: null, limit: null, note: null, @@ -941,9 +887,8 @@ export const billingJson: BillingV2Type = { }, { key: 'session_replay_network_payloads', - name: 'Network payload capture', - description: - 'Capture and analyze network requests and response payloads and headers for each session recording.', + name: 'Network monitor', + description: 'Analyze performance and network calls.', unit: null, limit: null, note: null, @@ -967,7 +912,7 @@ export const billingJson: BillingV2Type = { }, { key: 'replay_mask_sensitive_data', - name: 'Mask sensitive data', + name: 'Block sensitive data', description: 'Disable capturing data from any DOM element with HTML attributes or a customizable config.', unit: null, @@ -984,9 +929,8 @@ export const billingJson: BillingV2Type = { }, { key: 'replay_product_analytics_integration', - name: 'Product analytics integration', - description: - 'Jump into a playlist of session recordings directly from any time series in a graph. See when events happen in your recording timeline.', + name: 'Event timeline', + description: "See a history of everything that happened in a user's session.", unit: null, limit: null, note: null, @@ -1044,14 +988,16 @@ export const billingJson: BillingV2Type = { tiers: null, current_plan: false, included_if: null, + contact_support: null, + unit_amount_usd: null, }, { plan_key: 'paid-20231218', product_key: 'session_replay', - name: 'Session replay', + name: 'Paid', description: - 'Searchable recordings of people using your app or website with console logs and behavioral bucketing.', - image_url: 'https://posthog.com/images/product/product-icons/session-replay.svg', + 'Session replay helps you diagnose issues and understand user behavior in your product or website.', + image_url: 'https://posthog.com/images/products/session-replay/session-replay.png', docs_url: 'https://posthog.com/docs/session-replay', note: null, unit: 'recording', @@ -1060,7 +1006,7 @@ export const billingJson: BillingV2Type = { { key: 'console_logs', name: 'Console logs', - description: "Diagnose issues by inspecting errors in the user's network console", + description: "Debug issues faster by browsing the user's console.", unit: null, limit: null, note: null, @@ -1085,17 +1031,16 @@ export const billingJson: BillingV2Type = { }, { key: 'session_replay_network_payloads', - name: 'Network payload capture', - description: - 'Capture and analyze network requests and response payloads and headers for each session recording.', + name: 'Network monitor', + description: 'Analyze performance and network calls.', unit: null, limit: null, note: null, }, { key: 'recordings_file_export', - name: 'Recordings file export', - description: 'Save session recordings as a file to your local filesystem.', + name: 'Download recordings', + description: 'Retain recordings beyond data retention limits.', unit: null, limit: null, note: null, @@ -1110,7 +1055,7 @@ export const billingJson: BillingV2Type = { }, { key: 'replay_mask_sensitive_data', - name: 'Mask sensitive data', + name: 'Block sensitive data', description: 'Disable capturing data from any DOM element with HTML attributes or a customizable config.', unit: null, @@ -1127,9 +1072,8 @@ export const billingJson: BillingV2Type = { }, { key: 'replay_product_analytics_integration', - name: 'Product analytics integration', - description: - 'Jump into a playlist of session recordings directly from any time series in a graph. See when events happen in your recording timeline.', + name: 'Event timeline', + description: "See a history of everything that happened in a user's session.", unit: null, limit: null, note: null, @@ -1191,7 +1135,7 @@ export const billingJson: BillingV2Type = { up_to: 5000, current_amount_usd: '0.00', current_usage: 0, - projected_usage: 0, + projected_usage: null, projected_amount_usd: null, }, { @@ -1242,6 +1186,8 @@ export const billingJson: BillingV2Type = { ], current_plan: true, included_if: null, + contact_support: null, + unit_amount_usd: null, }, ], type: 'session_replay', @@ -1280,7 +1226,7 @@ export const billingJson: BillingV2Type = { up_to: 150000, current_amount_usd: '0.00', current_usage: 0, - projected_usage: 100000, + projected_usage: 10000, projected_amount_usd: '270.00', }, { @@ -1304,7 +1250,7 @@ export const billingJson: BillingV2Type = { ], tiered: true, unit_amount_usd: null, - current_amount_usd_before_addons: null, + current_amount_usd_before_addons: '0.00', current_amount_usd: '403.07', current_usage: 16022, usage_limit: 100000, @@ -1469,12 +1415,13 @@ export const billingJson: BillingV2Type = { }, { name: 'Feature flags & A/B testing', - headline: null, - description: 'Safely roll out new features and run experiments on changes.', + headline: 'Safely roll out features and A/B tests to specific users or groups', + description: + 'Test changes with small groups of users before rolling out wider. Analyze usage with product analytics and session replay.', price_description: null, usage_key: 'feature_flag_requests', - image_url: 'https://posthog.com/images/product/product-icons/feature-flags.svg', - screenshot_url: null, + image_url: 'https://posthog.com/images/products/feature-flags/feature-flags.png', + screenshot_url: 'https://posthog.com/images/products/feature-flags/screenshot-feature-flags.png', icon_key: 'IconToggle', docs_url: 'https://posthog.com/docs/feature-flags', subscribed: false, @@ -1482,9 +1429,10 @@ export const billingJson: BillingV2Type = { { plan_key: 'free-20230117', product_key: 'feature_flags', - name: 'Feature flags & A/B testing', - description: 'Safely roll out new features and run experiments on changes.', - image_url: 'https://posthog.com/images/product/product-icons/feature-flags.svg', + name: 'Free', + description: + 'Test changes with small groups of users before rolling out wider. Analyze usage with product analytics and session replay.', + image_url: 'https://posthog.com/images/products/feature-flags/feature-flags.png', docs_url: 'https://posthog.com/docs/feature-flags', note: null, unit: 'request', @@ -1498,6 +1446,15 @@ export const billingJson: BillingV2Type = { limit: null, note: null, }, + { + key: 'multivariate_flags', + name: 'Multivariate feature flags & experiments', + description: + 'Create three or more variants of a feature flag to test or release different versions of a feature.', + unit: null, + limit: null, + note: null, + }, { key: 'persist_flags_cross_authentication', name: 'Persist flags across authentication', @@ -1509,9 +1466,9 @@ export const billingJson: BillingV2Type = { }, { key: 'feature_flag_payloads', - name: 'Payloads', + name: 'Test changes without code', description: - 'Send additional pieces of information (any valid JSON) to your app when a flag is matched for a user.', + 'Use JSON payloads to change text, visuals, or entire blocks of code without subsequent deployments.', unit: null, limit: null, note: null, @@ -1520,7 +1477,7 @@ export const billingJson: BillingV2Type = { key: 'multiple_release_conditions', name: 'Multiple release conditions', description: - 'Target multiple groups of users with different release conditions for the same feature flag.', + 'Customize your rollout strategy by user or group properties, cohort, or trafic percentage.', unit: null, limit: null, note: null, @@ -1561,6 +1518,41 @@ export const billingJson: BillingV2Type = { limit: null, note: null, }, + { + key: 'experimentation', + name: 'A/B testing', + description: 'Test changes to your product and evaluate the impacts those changes make.', + unit: null, + limit: null, + note: null, + }, + { + key: 'funnel_experiments', + name: 'Funnel & trend experiments', + description: + 'Measure the impact of a change on a aggregate values or a series of events, like a signup flow.', + unit: null, + limit: null, + note: null, + }, + { + key: 'secondary_metrics', + name: 'Secondary experiment metrics', + description: + 'Track additional metrics to see how your experiment affects other parts of your app or different flows.', + unit: null, + limit: null, + note: null, + }, + { + key: 'statistical_analysis', + name: 'Statistical analysis', + description: + "Get a statistical analysis of your experiment results to see if the results are significant, or if they're likely just due to chance.", + unit: null, + limit: null, + note: null, + }, { key: 'feature_flags_data_retention', name: 'Data retention', @@ -1573,13 +1565,16 @@ export const billingJson: BillingV2Type = { tiers: null, current_plan: true, included_if: null, + contact_support: null, + unit_amount_usd: null, }, { plan_key: 'paid-20230623', product_key: 'feature_flags', - name: 'Feature flags & A/B testing', - description: 'Safely roll out new features and run experiments on changes.', - image_url: 'https://posthog.com/images/product/product-icons/feature-flags.svg', + name: 'Paid', + description: + 'Test changes with small groups of users before rolling out wider. Analyze usage with product analytics and session replay.', + image_url: 'https://posthog.com/images/products/feature-flags/feature-flags.png', docs_url: 'https://posthog.com/docs/feature-flags', note: null, unit: 'request', @@ -1613,9 +1608,9 @@ export const billingJson: BillingV2Type = { }, { key: 'feature_flag_payloads', - name: 'Payloads', + name: 'Test changes without code', description: - 'Send additional pieces of information (any valid JSON) to your app when a flag is matched for a user.', + 'Use JSON payloads to change text, visuals, or entire blocks of code without subsequent deployments.', unit: null, limit: null, note: null, @@ -1624,7 +1619,7 @@ export const billingJson: BillingV2Type = { key: 'multiple_release_conditions', name: 'Multiple release conditions', description: - 'Target multiple groups of users with different release conditions for the same feature flag.', + 'Customize your rollout strategy by user or group properties, cohort, or trafic percentage.', unit: null, limit: null, note: null, @@ -1673,15 +1668,6 @@ export const billingJson: BillingV2Type = { limit: null, note: null, }, - { - key: 'group_experiments', - name: 'Group experiments', - description: - 'Target experiments to specific groups of users so everyone in the same group gets the same variant.', - unit: null, - limit: null, - note: null, - }, { key: 'funnel_experiments', name: 'Funnel & trend experiments', @@ -1709,6 +1695,24 @@ export const billingJson: BillingV2Type = { limit: null, note: null, }, + { + key: 'group_experiments', + name: 'Group experiments', + description: + 'Target experiments to specific groups of users so everyone in the same group gets the same variant.', + unit: null, + limit: null, + note: null, + }, + { + key: 'multiple_environments', + name: 'Multi-environment support', + description: + 'Test flags in local development or staging by using the same flag key across PostHog projects.', + unit: null, + limit: null, + note: null, + }, { key: 'feature_flags_data_retention', name: 'Data retention', @@ -1767,6 +1771,8 @@ export const billingJson: BillingV2Type = { ], current_plan: false, included_if: null, + contact_support: null, + unit_amount_usd: null, }, ], type: 'feature_flags', @@ -1964,12 +1970,13 @@ export const billingJson: BillingV2Type = { }, { name: 'Surveys', - headline: null, - description: 'Collect feedback from your users. Multiple choice, rating, open text, and more.', + headline: 'Ask anything with no-code surveys', + description: + 'Build in-app popups with freeform text responses, multiple choice, NPS, ratings, and emoji reactions. Or use the API for complete control.', price_description: null, usage_key: 'survey_responses', - image_url: 'https://posthog.com/images/product/product-icons/surveys.svg', - screenshot_url: null, + image_url: 'https://posthog.com/images/products/surveys/surveys.png', + screenshot_url: 'https://posthog.com/images/products/surveys/screenshot-surveys.png', icon_key: 'IconMessage', docs_url: 'https://posthog.com/docs/surveys', subscribed: false, @@ -1977,9 +1984,10 @@ export const billingJson: BillingV2Type = { { plan_key: 'free-20230928', product_key: 'surveys', - name: 'Surveys', - description: 'Collect feedback from your users. Multiple choice, rating, open text, and more.', - image_url: 'https://posthog.com/images/product/product-icons/surveys.svg', + name: 'Free', + description: + 'Build in-app popups with freeform text responses, multiple choice, NPS, ratings, and emoji reactions. Or use the API for complete control.', + image_url: 'https://posthog.com/images/products/surveys/surveys.png', docs_url: 'https://posthog.com/docs/surveys', note: null, unit: 'survey response', @@ -2004,8 +2012,8 @@ export const billingJson: BillingV2Type = { }, { key: 'surveys_user_targeting', - name: 'User property targeting', - description: 'Target users based on any of their user properties.', + name: 'Advanced user targeting', + description: 'Target by URL, user property, or feature flag when used with Feature flags.', unit: null, limit: null, note: null, @@ -2021,15 +2029,17 @@ export const billingJson: BillingV2Type = { { key: 'surveys_api_mode', name: 'API mode', - description: 'Create surveys via the API.', + description: + 'Using PostHog.js? No more code required. But if want to create your own UI, we have a full API.', unit: null, limit: null, note: null, }, { key: 'surveys_results_analysis', - name: 'Results analysis', - description: 'Analyze your survey results including completion rates and drop offs.', + name: 'Aggregated results', + description: + 'See feedback summarized and broken down per response, plus completion rates and drop offs.', unit: null, limit: null, note: null, @@ -2055,13 +2065,16 @@ export const billingJson: BillingV2Type = { tiers: null, current_plan: true, included_if: null, + contact_support: null, + unit_amount_usd: null, }, { plan_key: 'paid-20230928', product_key: 'surveys', - name: 'Surveys', - description: 'Collect feedback from your users. Multiple choice, rating, open text, and more.', - image_url: 'https://posthog.com/images/product/product-icons/surveys.svg', + name: 'Paid', + description: + 'Build in-app popups with freeform text responses, multiple choice, NPS, ratings, and emoji reactions. Or use the API for complete control.', + image_url: 'https://posthog.com/images/products/surveys/surveys.png', docs_url: 'https://posthog.com/docs/surveys', note: null, unit: 'survey response', @@ -2087,15 +2100,15 @@ export const billingJson: BillingV2Type = { { key: 'surveys_multiple_questions', name: 'Multiple questions', - description: 'Create multiple questions in a single survey.', + description: 'Ask up to 10 questions in a single survey.', unit: null, limit: null, note: null, }, { key: 'surveys_user_targeting', - name: 'User property targeting', - description: 'Target users based on any of their user properties.', + name: 'Advanced user targeting', + description: 'Target by URL, user property, or feature flag when used with Feature flags.', unit: null, limit: null, note: null, @@ -2128,15 +2141,17 @@ export const billingJson: BillingV2Type = { { key: 'surveys_api_mode', name: 'API mode', - description: 'Create surveys via the API.', + description: + 'Using PostHog.js? No more code required. But if want to create your own UI, we have a full API.', unit: null, limit: null, note: null, }, { key: 'surveys_results_analysis', - name: 'Results analysis', - description: 'Analyze your survey results including completion rates and drop offs.', + name: 'Aggregated results', + description: + 'See feedback summarized and broken down per response, plus completion rates and drop offs.', unit: null, limit: null, note: null, @@ -2217,6 +2232,8 @@ export const billingJson: BillingV2Type = { ], current_plan: false, included_if: null, + contact_support: null, + unit_amount_usd: null, }, ], type: 'surveys', @@ -2388,7 +2405,7 @@ export const billingJson: BillingV2Type = { { plan_key: 'free-20230117', product_key: 'integrations', - name: 'Integrations', + name: 'Free', description: 'Connect PostHog to your favorite tools.', image_url: 'https://posthog.com/images/product/product-icons/integrations.svg', docs_url: 'https://posthog.com/docs/apps', @@ -2442,11 +2459,13 @@ export const billingJson: BillingV2Type = { tiers: null, current_plan: false, included_if: 'no_active_subscription', + contact_support: null, + unit_amount_usd: null, }, { plan_key: 'paid-20230117', product_key: 'integrations', - name: 'Integrations', + name: 'Paid', description: 'Connect PostHog to your favorite tools.', image_url: 'https://posthog.com/images/product/product-icons/integrations.svg', docs_url: 'https://posthog.com/docs/apps', @@ -2508,6 +2527,8 @@ export const billingJson: BillingV2Type = { tiers: null, current_plan: true, included_if: 'has_subscription', + contact_support: null, + unit_amount_usd: null, }, ], type: 'integrations', @@ -2595,7 +2616,7 @@ export const billingJson: BillingV2Type = { { plan_key: 'free-20230117', product_key: 'platform_and_support', - name: 'Platform and support', + name: 'Totally free', description: 'SSO, permission management, and support.', image_url: 'https://posthog.com/images/product/product-icons/platform.svg', docs_url: 'https://posthog.com/docs', @@ -2653,22 +2674,24 @@ export const billingJson: BillingV2Type = { note: null, }, { - key: 'terms_and_conditions', - name: 'Terms and conditions', - description: 'Terms and conditions', + key: '2fa', + name: '2FA', + description: 'Secure your PostHog account with two-factor authentication.', unit: null, limit: null, - note: 'Standard', + note: null, }, ], tiers: null, current_plan: false, included_if: 'no_active_subscription', + contact_support: null, + unit_amount_usd: null, }, { - plan_key: 'paid-20230926', + plan_key: 'paid-20240208', product_key: 'platform_and_support', - name: 'Platform and support', + name: 'With subscription', description: 'SSO, permission management, and support.', image_url: 'https://posthog.com/images/product/product-icons/platform.svg', docs_url: 'https://posthog.com/docs', @@ -2697,9 +2720,9 @@ export const billingJson: BillingV2Type = { name: 'Projects', description: 'Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.', - unit: null, - limit: null, - note: 'Unlimited', + unit: 'projects', + limit: 2, + note: null, }, { key: 'api_access', @@ -2718,69 +2741,483 @@ export const billingJson: BillingV2Type = { note: null, }, { - key: 'project_based_permissioning', - name: 'Project permissions', - description: 'Restrict access to data within the organization to only those who need it.', + key: 'community_support', + name: 'Community support', + description: 'Get help from other users and PostHog team members in our Community forums.', unit: null, limit: null, note: null, }, { - key: 'white_labelling', - name: 'White labeling', - description: 'Use your own branding in your PostHog organization.', + key: 'dedicated_support', + name: 'Dedicated account manager', + description: + 'Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.', + unit: null, + limit: null, + note: '$2k+/month spend', + }, + { + key: 'email_support', + name: 'Email support', + description: + 'Get help directly from our product engineers via email. No wading through multiple support people before you get help.', unit: null, limit: null, note: null, }, { - key: 'community_support', - name: 'Community support', - description: 'Get help from other users and PostHog team members in our Community forums.', + key: '2fa', + name: '2FA', + description: 'Secure your PostHog account with two-factor authentication.', unit: null, limit: null, note: null, }, + ], + tiers: null, + current_plan: true, + included_if: 'has_subscription', + contact_support: null, + unit_amount_usd: null, + }, + { + plan_key: 'teams-20240208', + product_key: 'platform_and_support', + name: 'Teams', + description: 'SSO, permission management, and support.', + image_url: 'https://posthog.com/images/product/product-icons/platform.svg', + docs_url: 'https://posthog.com/docs', + note: null, + unit: null, + free_allocation: null, + features: [ { - key: 'dedicated_support', - name: 'Slack (dedicated channel)', + key: 'tracked_users', + name: 'Tracked users', + description: 'Track users across devices and sessions.', + unit: null, + limit: null, + note: 'Unlimited', + }, + { + key: 'team_members', + name: 'Team members', + description: "PostHog doesn't charge per seat add your entire team!", + unit: null, + limit: null, + note: 'Unlimited', + }, + { + key: 'organizations_projects', + name: 'Projects', description: - 'Get help directly from our support team in a dedicated Slack channel shared between you and the PostHog team.', + 'Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.', unit: null, limit: null, - note: '$2k/month spend or above', + note: 'Unlimited', }, { - key: 'email_support', - name: 'Direct access to engineers', + key: 'api_access', + name: 'API access', + description: 'Access your data via our developer-friendly API.', + unit: null, + limit: null, + note: null, + }, + { + key: 'social_sso', + name: 'SSO via Google, Github, or Gitlab', + description: 'Log in to PostHog with your Google, Github, or Gitlab account.', + unit: null, + limit: null, + note: null, + }, + { + key: 'sso_enforcement', + name: 'Enforce SSO login', description: - 'Get help directly from our product engineers via email. No wading through multiple support people before you get help.', + 'Users can only sign up and log in to your PostHog organization with your specified SSO provider.', unit: null, limit: null, note: null, }, { - key: 'terms_and_conditions', - name: 'Terms and conditions', - description: 'Terms and conditions', + key: '2fa', + name: '2FA', + description: 'Secure your PostHog account with two-factor authentication.', unit: null, limit: null, - note: 'Standard', + note: null, }, { - key: 'security_assessment', - name: 'Security assessment', - description: 'Security assessment', + key: '2fa_enforcement', + name: 'Enforce 2FA', + description: 'Require all users in your organization to enable two-factor authentication.', unit: null, limit: null, note: null, }, - ], - tiers: null, - current_plan: true, - included_if: 'has_subscription', - }, - ], + { + key: 'community_support', + name: 'Community support', + description: 'Get help from other users and PostHog team members in our Community forums.', + unit: null, + limit: null, + note: null, + }, + { + key: 'email_support', + name: 'Email support', + description: + 'Get help directly from our product engineers via email. No wading through multiple support people before you get help.', + unit: null, + limit: null, + note: null, + }, + { + key: 'dedicated_support', + name: 'Dedicated account manager', + description: + 'Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.', + unit: null, + limit: null, + note: '$2k+/month spend', + }, + { + key: 'priority_support', + name: 'Priority support', + description: 'Get help from our team faster than other customers.', + unit: null, + limit: null, + note: null, + }, + { + key: 'white_labelling', + name: 'White labeling', + description: + 'Use your own branding on surveys, shared dashboards, shared insights, and more.', + unit: null, + limit: null, + note: null, + }, + { + key: 'project_based_permissioning', + name: 'Project permissions', + description: 'Restrict access to data within the organization to only those who need it.', + unit: null, + limit: null, + note: null, + }, + { + key: 'advanced_permissions', + name: 'Advanced permissions', + description: + 'Control who can access and modify data and features within your organization.', + unit: null, + limit: null, + note: 'Project-based only', + }, + { + key: 'audit_logs', + name: 'Audit logs', + description: + 'See who in your organization has accessed or modified entities within PostHog.', + unit: null, + limit: null, + note: 'Basic', + }, + { + key: 'security_assessment', + name: 'Security assessment', + description: 'Security assessment', + unit: null, + limit: null, + note: null, + }, + { + key: 'hipaa_baa', + name: 'HIPAA BAA', + description: + 'Get a signed HIPAA Business Associate Agreement (BAA) to use PostHog in a HIPAA-compliant manner.', + unit: null, + limit: null, + note: null, + }, + { + key: 'team_collaboration', + name: 'Team collaboration features', + description: + 'Work together better with tags on dashboards and insights; descriptions on insights, events, & properties; verified events; comments on almost anything.', + unit: null, + limit: null, + note: null, + }, + { + key: 'ingestion_taxonomy', + name: 'Ingestion taxonomy', + description: + 'Mark events as verified or unverified to help you understand the quality of your data.', + unit: null, + limit: null, + note: null, + }, + { + key: 'tagging', + name: 'Dashboard tags', + description: 'Organize dashboards with tags.', + unit: null, + limit: null, + note: null, + }, + ], + tiers: [], + current_plan: false, + included_if: null, + contact_support: null, + unit_amount_usd: '450.00', + }, + { + plan_key: 'enterprise-20240208', + product_key: 'platform_and_support', + name: 'Enterprise', + description: 'SSO, permission management, and support.', + image_url: 'https://posthog.com/images/product/product-icons/platform.svg', + docs_url: 'https://posthog.com/docs', + note: null, + unit: null, + free_allocation: null, + features: [ + { + key: 'team_members', + name: 'Team members', + description: "PostHog doesn't charge per seat add your entire team!", + unit: null, + limit: null, + note: 'Unlimited', + }, + { + key: 'organizations_projects', + name: 'Projects', + description: + 'Create silos of data within PostHog. All data belongs to a single project and all queries are project-specific.', + unit: null, + limit: null, + note: 'Unlimited', + }, + { + key: 'tracked_users', + name: 'Tracked users', + description: 'Track users across devices and sessions.', + unit: null, + limit: null, + note: 'Unlimited', + }, + { + key: 'api_access', + name: 'API access', + description: 'Access your data via our developer-friendly API.', + unit: null, + limit: null, + note: null, + }, + { + key: 'white_labelling', + name: 'White labeling', + description: + 'Use your own branding on surveys, shared dashboards, shared insights, and more.', + unit: null, + limit: null, + note: null, + }, + { + key: 'team_collaboration', + name: 'Team collaboration features', + description: + 'Work together better with tags on dashboards and insights; descriptions on insights, events, & properties; verified events; comments on almost anything.', + unit: null, + limit: null, + note: null, + }, + { + key: 'ingestion_taxonomy', + name: 'Ingestion taxonomy', + description: + 'Mark events as verified or unverified to help you understand the quality of your data.', + unit: null, + limit: null, + note: null, + }, + { + key: 'tagging', + name: 'Dashboard tags', + description: 'Organize dashboards with tags.', + unit: null, + limit: null, + note: null, + }, + { + key: 'social_sso', + name: 'SSO via Google, Github, or Gitlab', + description: 'Log in to PostHog with your Google, Github, or Gitlab account.', + unit: null, + limit: null, + note: null, + }, + { + key: 'sso_enforcement', + name: 'Enforce SSO login', + description: + 'Users can only sign up and log in to your PostHog organization with your specified SSO provider.', + unit: null, + limit: null, + note: null, + }, + { + key: 'saml', + name: 'SAML SSO', + description: "Allow your organization's users to log in with SAML.", + unit: null, + limit: null, + note: null, + }, + { + key: '2fa', + name: '2FA', + description: 'Secure your PostHog account with two-factor authentication.', + unit: null, + limit: null, + note: null, + }, + { + key: '2fa_enforcement', + name: 'Enforce 2FA', + description: 'Require all users in your organization to enable two-factor authentication.', + unit: null, + limit: null, + note: null, + }, + { + key: 'project_based_permissioning', + name: 'Project permissions', + description: 'Restrict access to data within the organization to only those who need it.', + unit: null, + limit: null, + note: null, + }, + { + key: 'role_based_access', + name: 'Role-based access', + description: + 'Control access to features like experiments, session recordings, and feature flags with custom roles.', + unit: null, + limit: null, + note: null, + }, + { + key: 'advanced_permissions', + name: 'Advanced permissions', + description: + 'Control who can access and modify data and features within your organization.', + unit: null, + limit: null, + note: null, + }, + { + key: 'audit_logs', + name: 'Audit logs', + description: + 'See who in your organization has accessed or modified entities within PostHog.', + unit: null, + limit: null, + note: 'Advanced', + }, + { + key: 'hipaa_baa', + name: 'HIPAA BAA', + description: + 'Get a signed HIPAA Business Associate Agreement (BAA) to use PostHog in a HIPAA-compliant manner.', + unit: null, + limit: null, + note: null, + }, + { + key: 'custom_msa', + name: 'Custom MSA', + description: + "Get a custom Master Services Agreement (MSA) to use PostHog in a way that fits your company's needs.", + unit: null, + limit: null, + note: null, + }, + { + key: 'community_support', + name: 'Community support', + description: 'Get help from other users and PostHog team members in our Community forums.', + unit: null, + limit: null, + note: null, + }, + { + key: 'email_support', + name: 'Email support', + description: + 'Get help directly from our product engineers via email. No wading through multiple support people before you get help.', + unit: null, + limit: null, + note: null, + }, + { + key: 'dedicated_support', + name: 'Dedicated account manager', + description: + 'Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.', + unit: null, + limit: null, + note: null, + }, + { + key: 'priority_support', + name: 'Priority support', + description: 'Get help from our team faster than other customers.', + unit: null, + limit: null, + note: null, + }, + { + key: 'security_assessment', + name: 'Security assessment', + description: 'Security assessment', + unit: null, + limit: null, + note: null, + }, + { + key: 'training', + name: 'Ongoing training', + description: + 'Get training from our team to help you quickly get up and running with PostHog.', + unit: null, + limit: null, + note: null, + }, + { + key: 'configuration_support', + name: 'Personalized onboarding', + description: + 'Get help from our team to create dashboards that will help you understand your data and your business.', + unit: null, + limit: null, + note: null, + }, + ], + tiers: null, + current_plan: false, + included_if: null, + contact_support: true, + unit_amount_usd: null, + }, + ], type: 'platform_and_support', free_allocation: 0, tiers: null, @@ -2796,7 +3233,7 @@ export const billingJson: BillingV2Type = { projected_amount_usd: null, unit: null, addons: [], - contact_support: true, + contact_support: false, inclusion_only: true, features: [ { @@ -2832,6 +3269,14 @@ export const billingJson: BillingV2Type = { icon_key: null, type: null, }, + { + key: 'social_sso', + name: 'SSO via Google, Github, or Gitlab', + description: 'Log in to PostHog with your Google, Github, or Gitlab account.', + images: null, + icon_key: null, + type: null, + }, { key: 'role_based_access', name: 'Role-based access', @@ -2842,17 +3287,17 @@ export const billingJson: BillingV2Type = { type: null, }, { - key: 'social_sso', - name: 'SSO via Google, Github, or Gitlab', - description: 'Log in to PostHog with your Google, Github, or Gitlab account.', + key: 'project_based_permissioning', + name: 'Project permissions', + description: 'Restrict access to data within the organization to only those who need it.', images: null, icon_key: null, type: null, }, { - key: 'project_based_permissioning', - name: 'Project permissions', - description: 'Restrict access to data within the organization to only those who need it.', + key: 'advanced_permissions', + name: 'Advanced permissions', + description: 'Control who can access and modify data and features within your organization.', images: null, icon_key: null, type: null, @@ -2874,10 +3319,26 @@ export const billingJson: BillingV2Type = { icon_key: null, type: null, }, + { + key: '2fa', + name: '2FA', + description: 'Secure your PostHog account with two-factor authentication.', + images: null, + icon_key: null, + type: null, + }, + { + key: '2fa_enforcement', + name: 'Enforce 2FA', + description: 'Require all users in your organization to enable two-factor authentication.', + images: null, + icon_key: null, + type: null, + }, { key: 'white_labelling', name: 'White labeling', - description: 'Use your own branding in your PostHog organization.', + description: 'Use your own branding on surveys, shared dashboards, shared insights, and more.', images: null, icon_key: null, type: null, @@ -2892,16 +3353,16 @@ export const billingJson: BillingV2Type = { }, { key: 'dedicated_support', - name: 'Slack (dedicated channel)', + name: 'Dedicated account manager', description: - 'Get help directly from our support team in a dedicated Slack channel shared between you and the PostHog team.', + 'Work with a dedicated account manager via Slack or email to help you get the most out of PostHog.', images: null, icon_key: null, type: null, }, { key: 'email_support', - name: 'Direct access to engineers', + name: 'Email support', description: 'Get help directly from our product engineers via email. No wading through multiple support people before you get help.', images: null, @@ -2909,16 +3370,16 @@ export const billingJson: BillingV2Type = { type: null, }, { - key: 'account_manager', - name: 'Account manager', - description: 'Work with a dedicated account manager to help you get the most out of PostHog.', + key: 'priority_support', + name: 'Priority support', + description: 'Get help from our team faster than other customers.', images: null, icon_key: null, type: null, }, { key: 'training', - name: 'Training sessions', + name: 'Ongoing training', description: 'Get training from our team to help you quickly get up and running with PostHog.', images: null, icon_key: null, @@ -2926,7 +3387,7 @@ export const billingJson: BillingV2Type = { }, { key: 'configuration_support', - name: 'Dashboard configuration support', + name: 'Personalized onboarding', description: 'Get help from our team to create dashboards that will help you understand your data and your business.', images: null, @@ -2973,11 +3434,62 @@ export const billingJson: BillingV2Type = { icon_key: null, type: null, }, + { + key: 'audit_logs', + name: 'Audit logs', + description: 'See who in your organization has accessed or modified entities within PostHog.', + images: null, + icon_key: null, + type: null, + }, + { + key: 'hipaa_baa', + name: 'HIPAA BAA', + description: + 'Get a signed HIPAA Business Associate Agreement (BAA) to use PostHog in a HIPAA-compliant manner.', + images: null, + icon_key: null, + type: null, + }, + { + key: 'custom_msa', + name: 'Custom MSA', + description: + "Get a custom Master Services Agreement (MSA) to use PostHog in a way that fits your company's needs.", + images: null, + icon_key: null, + type: null, + }, + { + key: 'team_collaboration', + name: 'Team collaboration features', + description: + 'Work together better with tags on dashboards and insights; descriptions on insights, events, & properties; verified events; comments on almost anything.', + images: null, + icon_key: null, + type: null, + }, + { + key: 'ingestion_taxonomy', + name: 'Ingestion taxonomy', + description: + 'Mark events as verified or unverified to help you understand the quality of your data.', + images: null, + icon_key: null, + type: null, + }, + { + key: 'tagging', + name: 'Dashboard tags', + description: 'Organize dashboards with tags.', + images: null, + icon_key: null, + type: null, + }, ], }, ], - custom_limits_usd: { - session_replay: '700', - product_analytics: '550', - }, + custom_limits_usd: {}, + stripe_portal_url: + 'https://billing.stripe.com/p/session/test_YWNjdF8xSElNRERFdUlhdFJYU2R6LF9QaEVJR3VyemlvMDZzRzdiQXZrc1AxSjNXZk1BellP0100ZsforDQG', } diff --git a/frontend/src/scenes/billing/Billing.stories.tsx b/frontend/src/scenes/billing/Billing.stories.tsx index ff420d9db1c51..7ccd862eb111a 100644 --- a/frontend/src/scenes/billing/Billing.stories.tsx +++ b/frontend/src/scenes/billing/Billing.stories.tsx @@ -116,6 +116,7 @@ export const BillingUnsubscribeModal_DataPipelines = (): JSX.Element => { projected_amount_usd: '0', plans: [], usage_key: '', + contact_support: false, }, ] diff --git a/frontend/src/scenes/billing/Billing.tsx b/frontend/src/scenes/billing/Billing.tsx index e3e5d3575500f..e418e66c52309 100644 --- a/frontend/src/scenes/billing/Billing.tsx +++ b/frontend/src/scenes/billing/Billing.tsx @@ -6,15 +6,18 @@ import clsx from 'clsx' import { useActions, useValues } from 'kea' import { Field, Form } from 'kea-forms' import { router } from 'kea-router' +import { BillingUpgradeCTA } from 'lib/components/BillingUpgradeCTA' import { SurprisedHog } from 'lib/components/hedgehogs' import { PageHeader } from 'lib/components/PageHeader' import { supportLogic } from 'lib/components/Support/supportLogic' +import { FEATURE_FLAGS } from 'lib/constants' import { dayjs } from 'lib/dayjs' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { useEffect } from 'react' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { SceneExport } from 'scenes/sceneTypes' @@ -48,6 +51,7 @@ export function Billing(): JSX.Element { const { reportBillingV2Shown } = useActions(billingLogic) const { preflight, isCloudOrDev } = useValues(preflightLogic) const { openSupportForm } = useActions(supportLogic) + const { featureFlags } = useValues(featureFlagLogic) if (preflight && !isCloudOrDev) { router.actions.push(urls.default()) @@ -310,14 +314,22 @@ export function Billing(): JSX.Element {

Products

{isOnboarding && upgradeAllProductsLink && ( - } to={upgradeAllProductsLink} disableClientSideRouting > - Upgrade all - + {featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'subscribe' + ? 'Subscribe to all' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + !billing?.has_active_subscription + ? 'Add credit card to all products' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + billing?.has_active_subscription + ? 'Add all products to plan' + : 'Upgrade to all'}{' '} + )}
diff --git a/frontend/src/scenes/billing/BillingHero.tsx b/frontend/src/scenes/billing/BillingHero.tsx index ca8e8170a5832..726bc4775a251 100644 --- a/frontend/src/scenes/billing/BillingHero.tsx +++ b/frontend/src/scenes/billing/BillingHero.tsx @@ -1,10 +1,17 @@ import './BillingHero.scss' +import { useValues } from 'kea' import { BlushingHog } from 'lib/components/hedgehogs' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import useResizeObserver from 'use-resize-observer' +import { billingLogic } from './billingLogic' + export const BillingHero = (): JSX.Element => { const { width, ref: billingHeroRef } = useResizeObserver() + const { featureFlags } = useValues(featureFlagLogic) + const { billing } = useValues(billingLogic) return (
@@ -13,8 +20,17 @@ export const BillingHero = (): JSX.Element => {

Get the whole hog.

Only pay for what you use.

- Add your credit card details to get access to premium product and platform features. Set billing - limits as low as $0 to control spend. + {featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'subscribe' + ? 'Subscribe' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + !billing?.has_active_subscription + ? 'Add your credit card' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + billing?.has_active_subscription + ? 'Add the paid plan' + : 'Upgrade'}{' '} + to get access to premium product and platform features. Set billing limits as low as $0 to control + spend.

{width && width > 500 && ( diff --git a/frontend/src/scenes/billing/BillingProduct.tsx b/frontend/src/scenes/billing/BillingProduct.tsx index 78a7754854652..39e5fc0c3d63e 100644 --- a/frontend/src/scenes/billing/BillingProduct.tsx +++ b/frontend/src/scenes/billing/BillingProduct.tsx @@ -2,7 +2,8 @@ import { IconCheckCircle, IconChevronDown, IconDocument, IconInfo, IconPlus } fr import { LemonButton, LemonSelectOptions, LemonTable, LemonTag, Link } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' +import { BillingUpgradeCTA } from 'lib/components/BillingUpgradeCTA' +import { FEATURE_FLAGS, UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' import { IconChevronRight } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' @@ -211,6 +212,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): } = useActions(billingProductLogic({ product, productRef })) const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) + const { featureFlags } = useValues(featureFlagLogic) const upgradePlan = currentAndUpgradePlans?.upgradePlan const currentPlan = currentAndUpgradePlans?.currentPlan const downgradePlan = currentAndUpgradePlans?.downgradePlan @@ -609,8 +611,18 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): {additionalFeaturesOnUpgradedPlan?.length > 0 ? ( <>

- {!upgradePlan ? 'You now' : `Upgrade to the ${upgradePlan.name} plan to`} get - sweet features such as: + {product.subscribed + ? 'You now' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'subscribe' + ? 'Subscribe to' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + !billing?.has_active_subscription + ? 'Add a credit card to' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + billing?.has_active_subscription + ? 'Add paid plan' + : 'Upgrade to'}{' '} + get sweet features such as:

{additionalFeaturesOnUpgradedPlan?.map((feature, i) => { @@ -682,7 +694,8 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): ) : ( upgradePlan.included_if !== 'has_subscription' && !upgradePlan.unit_amount_usd && ( - - Upgrade - + {featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'subscribe' + ? 'Subscribe' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === + 'credit_card' && !billing?.has_active_subscription + ? 'Add credit card' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === + 'credit_card' && billing?.has_active_subscription + ? 'Add paid plan' + : 'Upgrade'} + ) )}
diff --git a/frontend/src/scenes/billing/PlanComparison.tsx b/frontend/src/scenes/billing/PlanComparison.tsx index 09cd31af31e21..a185b4df0277f 100644 --- a/frontend/src/scenes/billing/PlanComparison.tsx +++ b/frontend/src/scenes/billing/PlanComparison.tsx @@ -1,12 +1,14 @@ import './PlanComparison.scss' import { IconCheckCircle, IconWarning, IconX } from '@posthog/icons' -import { LemonButton, LemonModal, LemonTag, Link } from '@posthog/lemon-ui' +import { LemonModal, LemonTag, Link } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' +import { BillingUpgradeCTA } from 'lib/components/BillingUpgradeCTA' +import { FEATURE_FLAGS, UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' import { dayjs } from 'lib/dayjs' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import React from 'react' import { getProductIcon } from 'scenes/products/Products' @@ -119,6 +121,7 @@ export const PlanComparison = ({ const { billing, redirectPath } = useValues(billingLogic) const { width, ref: planComparisonRef } = useResizeObserver() const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) + const { featureFlags } = useValues(featureFlagLogic) const currentPlanIndex = plans.findIndex((plan) => plan.current_plan) const { surveyID, comparisonModalHighlightedFeatureKey } = useValues(billingProductLogic({ product })) const { reportSurveyShown, setSurveyResponse } = useActions(billingProductLogic({ product })) @@ -131,7 +134,7 @@ export const PlanComparison = ({ const upgradeButtons = plans?.map((plan, i) => { return ( - {plan.current_plan ? 'Current plan' @@ -182,8 +186,18 @@ export const PlanComparison = ({ i >= currentPlanIndex && !billing?.has_active_subscription ? 'View products' - : 'Subscribe'} - + : plan.free_allocation && !plan.tiers + ? 'Select' // Free plan + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'subscribe' + ? 'Subscribe' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + !billing?.has_active_subscription + ? 'Add credit card' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + billing?.has_active_subscription + ? 'Add paid plan' + : 'Upgrade'} + {!plan.current_plan && !plan.free_allocation && includeAddons && product.addons?.length > 0 && (

- or subscribe without addons + or{' '} + {featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'subscribe' + ? 'subscribe' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + !billing?.has_active_subscription + ? 'add credit card' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + !billing?.has_active_subscription + ? 'add paid plan' + : 'upgrade'}{' '} + without addons

)} diff --git a/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx b/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx index 34e63deaf130d..414287bdcaa36 100644 --- a/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx +++ b/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx @@ -206,8 +206,9 @@ export const viewLinkLogic = kea([ }, ], selectedSourceTable: [ - (s) => [s.selectedSourceTableName, s.tables], - (selectedSourceTableName, tables) => tables.find((row) => row.name === selectedSourceTableName), + (s) => [s.selectedSourceTableName, s.tables, s.savedQueries], + (selectedSourceTableName, tables, savedQueries) => + [...tables, ...savedQueries].find((row) => row.name === selectedSourceTableName), ], selectedJoiningTable: [ (s) => [s.selectedJoiningTableName, s.tables], @@ -234,12 +235,17 @@ export const viewLinkLogic = kea([ }, ], tableOptions: [ - (s) => [s.tables], - (tables) => - tables.map((table) => ({ + (s) => [s.tables, s.savedQueries], + (tables, savedQueries) => [ + ...tables.map((table) => ({ value: table.name, label: table.name, })), + ...savedQueries.map((query) => ({ + value: query.name, + label: query.name, + })), + ], ], sourceTableKeys: [ (s) => [s.selectedSourceTable], diff --git a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts index c743a8bf4848e..9614c49c7542a 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts @@ -13,7 +13,6 @@ import { EntityType, EntityTypes, FilterType, - InsightShortId, } from '~/types' import type { entityFilterLogicType } from './entityFilterLogicType' @@ -73,19 +72,9 @@ export const entityFilterLogic = kea([ props({} as EntityFilterProps), key((props) => props.typeKey), path((key) => ['scenes', 'insights', 'ActionFilter', 'entityFilterLogic', key]), - connect((props: EntityFilterProps) => ({ + connect({ logic: [eventUsageLogic], - actions: [ - insightDataLogic({ - dashboardItemId: props.typeKey as InsightShortId, - // this can be mounted in replay filters - // in which case there's not really an insightDataLogic to mount - // disable attempts to load data that will never work - doNotLoad: props.typeKey === 'session-recordings', - }), - ['loadData'], - ], - })), + }), actions({ selectFilter: (filter: EntityFilter | ActionFilter | null) => ({ filter }), updateFilterMath: ( @@ -193,7 +182,10 @@ export const entityFilterLogic = kea([ await breakpoint(100) - actions.loadData(true) + const dataLogic = insightDataLogic.findMounted({ + dashboardItemId: props.typeKey, + }) + dataLogic?.actions?.loadData(true) }, hideModal: () => { actions.selectFilter(null) diff --git a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx index be78629f06f26..a5ac9fc397506 100644 --- a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx @@ -1,8 +1,11 @@ import { IconCheckCircle } from '@posthog/icons' import { LemonBanner, LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { BillingUpgradeCTA } from 'lib/components/BillingUpgradeCTA' import { StarHog } from 'lib/components/hedgehogs' +import { FEATURE_FLAGS } from 'lib/constants' import { Spinner } from 'lib/lemon-ui/Spinner' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { useState } from 'react' import { getUpgradeProductLink } from 'scenes/billing/billing-utils' @@ -29,6 +32,7 @@ export const OnboardingBillingStep = ({ const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) const plan = currentAndUpgradePlans?.upgradePlan const currentPlan = currentAndUpgradePlans?.currentPlan + const { featureFlags } = useValues(featureFlagLogic) const [showPlanComp, setShowPlanComp] = useState(false) @@ -39,7 +43,7 @@ export const OnboardingBillingStep = ({ stepKey={stepKey} continueOverride={ product?.subscribed ? undefined : ( - { reportBillingUpgradeClicked(product.type) }} + data-attr="onboarding-subscribe-button" > - Subscribe to Paid Plan - + {featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'subscribe' + ? 'Subscribe to paid plan' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + !billing?.has_active_subscription + ? 'Add credit card to get paid features' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'credit_card' && + billing?.has_active_subscription + ? 'Add paid plan' + : 'Upgrade to paid plan'} + ) } > @@ -63,7 +76,17 @@ export const OnboardingBillingStep = ({
-

Subscribe successful

+

+ {featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === 'subscribe' + ? 'Subscribe successful' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === + 'credit_card' && !billing?.has_active_subscription + ? 'Successfully added credit card' + : featureFlags[FEATURE_FLAGS.BILLING_UPGRADE_LANGUAGE] === + 'credit_card' && !billing?.has_active_subscription + ? 'Successfully added paid plan' + : 'Upgrade successful'} +

You're all ready to use {product.name}.

@@ -71,7 +94,11 @@ export const OnboardingBillingStep = ({
- setShowPlanComp(!showPlanComp)}> + setShowPlanComp(!showPlanComp)} + > {showPlanComp ? 'Hide' : 'Show'} plans {currentPlan?.initial_billing_limit && ( diff --git a/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx b/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx index 5402f8fe7d728..0daad6c18d479 100644 --- a/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx +++ b/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx @@ -96,7 +96,7 @@ const GetStartedButton = ({ product }: { product: BillingProductV2Type }): JSX.E {(!hasSnippetEvents || multiInstallProducts.includes(product.type as ProductKey)) && ( { setTeamPropertiesForProduct(product.type as ProductKey) reportOnboardingProductSelected( diff --git a/frontend/src/scenes/onboarding/OnboardingStep.tsx b/frontend/src/scenes/onboarding/OnboardingStep.tsx index c4cd544e7a38d..ddb081b343919 100644 --- a/frontend/src/scenes/onboarding/OnboardingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingStep.tsx @@ -96,6 +96,7 @@ export const OnboardingStep = ({ onSkip && onSkip() !hasNextStep ? completeOnboarding() : goToNextStep() }} + data-attr="onboarding-skip-button" > Skip {!hasNextStep ? 'and finish' : 'for now'} @@ -106,6 +107,7 @@ export const OnboardingStep = ({ { continueAction && continueAction() !hasNextStep ? completeOnboarding() : goToNextStep() diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index f6b597b74414a..8857c73b7fe42 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -1,7 +1,6 @@ import { actions, connect, kea, listeners, path, props, reducers, selectors } from 'kea' -import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router' -import { FEATURE_FLAGS } from 'lib/constants' -import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic' +import { actionToUrl, router, urlToAction } from 'kea-router' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { billingLogic } from 'scenes/billing/billingLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' @@ -62,13 +61,10 @@ export const stepKeyToTitle = (stepKey?: OnboardingStepKey): undefined | string export type AllOnboardingSteps = OnboardingStep[] export type OnboardingStep = JSX.Element -export const getProductUri = (productKey: ProductKey, featureFlags?: FeatureFlagsSet): string => { +export const getProductUri = (productKey: ProductKey): string => { switch (productKey) { case ProductKey.PRODUCT_ANALYTICS: - return featureFlags && - featureFlags[FEATURE_FLAGS.REDIRECT_INSIGHT_CREATION_PRODUCT_ANALYTICS_ONBOARDING] === 'test' - ? urls.insightNew() - : combineUrl(urls.insights(), {}, { panel: 'activation' }).url + return urls.insightNew() case ProductKey.SESSION_REPLAY: return urls.replay() case ProductKey.FEATURE_FLAGS: @@ -168,9 +164,9 @@ export const onboardingLogic = kea([ }, ], onCompleteOnboardingRedirectUrl: [ - (s) => [s.featureFlags, s.productKey], - (featureFlags: FeatureFlagsSet, productKey: string | null) => { - return productKey ? getProductUri(productKey as ProductKey, featureFlags) : urls.default() + (s) => [s.productKey], + (productKey: string | null) => { + return productKey ? getProductUri(productKey as ProductKey) : urls.default() }, ], totalOnboardingSteps: [ diff --git a/frontend/src/scenes/onboarding/sdks/SDKs.tsx b/frontend/src/scenes/onboarding/sdks/SDKs.tsx index d885e94ea6fcf..33555a1f17ca9 100644 --- a/frontend/src/scenes/onboarding/sdks/SDKs.tsx +++ b/frontend/src/scenes/onboarding/sdks/SDKs.tsx @@ -86,6 +86,7 @@ export function SDKs({ ) : ( <> : null} type="primary" status="alt" diff --git a/frontend/src/scenes/products/Products.tsx b/frontend/src/scenes/products/Products.tsx index c1f7311425a05..3eade1085f70e 100644 --- a/frontend/src/scenes/products/Products.tsx +++ b/frontend/src/scenes/products/Products.tsx @@ -5,7 +5,6 @@ import { useActions, useValues } from 'kea' import { router } from 'kea-router' import { LemonCard } from 'lib/lemon-ui/LemonCard/LemonCard' import { Spinner } from 'lib/lemon-ui/Spinner' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { billingLogic } from 'scenes/billing/billingLogic' import { getProductUri, onboardingLogic } from 'scenes/onboarding/onboardingLogic' @@ -44,7 +43,6 @@ export function ProductCard({ className?: string }): JSX.Element { const { currentTeam } = useValues(teamLogic) - const { featureFlags } = useValues(featureFlagLogic) const { setIncludeIntro } = useActions(onboardingLogic) const { user } = useValues(userLogic) const { reportOnboardingProductSelected } = useActions(eventUsageLogic) @@ -77,8 +75,9 @@ export function ProductCard({ className="relative" onClick={(e) => { e.stopPropagation() - router.actions.push(getProductUri(product.type as ProductKey, featureFlags)) + router.actions.push(getProductUri(product.type as ProductKey)) }} + data-attr={`return-to-${product.type}`} > diff --git a/frontend/src/scenes/sceneLogic.ts b/frontend/src/scenes/sceneLogic.ts index bfb5fe46210da..998726e131ec5 100644 --- a/frontend/src/scenes/sceneLogic.ts +++ b/frontend/src/scenes/sceneLogic.ts @@ -4,14 +4,13 @@ import { commandBarLogic } from 'lib/components/CommandBar/commandBarLogic' import { BarStatus } from 'lib/components/CommandBar/types' import { FEATURE_FLAGS, TeamMembershipLevel } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { addProjectIdIfMissing, removeProjectIdIfPresent } from 'lib/utils/router-utils' import posthog from 'posthog-js' import { emptySceneParams, preloadedScenes, redirects, routes, sceneConfigurations } from 'scenes/scenes' import { LoadedScene, Params, Scene, SceneConfig, SceneExport, SceneParams } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { AvailableFeature, ProductKey } from '~/types' +import { ProductKey } from '~/types' import { handleLoginRedirect } from './authentication/loginLogic' import { onboardingLogic, OnboardingStepKey } from './onboarding/onboardingLogic' @@ -52,27 +51,6 @@ export const sceneLogic = kea([ setLoadedScene: (loadedScene: LoadedScene) => ({ loadedScene, }), - showUpgradeModal: (featureKey: AvailableFeature, currentUsage?: number, isGrandfathered?: boolean) => ({ - featureKey, - currentUsage, - isGrandfathered, - }), - guardAvailableFeature: ( - featureKey: AvailableFeature, - featureAvailableCallback?: () => void, - guardOn: { - cloud: boolean - selfHosted: boolean - } = { - cloud: true, - selfHosted: true, - }, - // how much of the feature has been used (eg. number of recording playlists created), - // which will be compared to the limit for their subscriptions - currentUsage?: number, - isGrandfathered?: boolean - ) => ({ featureKey, featureAvailableCallback, guardOn, currentUsage, isGrandfathered }), - hideUpgradeModal: true, reloadBrowserDueToImportError: true, }), reducers({ @@ -105,27 +83,6 @@ export const sceneLogic = kea([ setScene: () => null, }, ], - upgradeModalFeatureKey: [ - null as AvailableFeature | null, - { - showUpgradeModal: (_, { featureKey }) => featureKey, - hideUpgradeModal: () => null, - }, - ], - upgradeModalFeatureUsage: [ - null as number | null, - { - showUpgradeModal: (_, { currentUsage }) => currentUsage ?? null, - hideUpgradeModal: () => null, - }, - ], - upgradeModalIsGrandfathered: [ - null as boolean | null, - { - showUpgradeModal: (_, { isGrandfathered }) => isGrandfathered ?? null, - hideUpgradeModal: () => null, - }, - ], lastReloadAt: [ null as number | null, { persist: true }, @@ -170,27 +127,6 @@ export const sceneLogic = kea([ hashParams: [(s) => [s.sceneParams], (sceneParams): Record => sceneParams.hashParams || {}], }), listeners(({ values, actions, props, selectors }) => ({ - showUpgradeModal: ({ featureKey }) => { - eventUsageLogic.actions.reportUpgradeModalShown(featureKey) - }, - guardAvailableFeature: ({ featureKey, featureAvailableCallback, guardOn, currentUsage, isGrandfathered }) => { - const { preflight } = preflightLogic.values - let featureAvailable: boolean - if (!preflight) { - featureAvailable = false - } else if (!guardOn.cloud && preflight.cloud) { - featureAvailable = true - } else if (!guardOn.selfHosted && !preflight.cloud) { - featureAvailable = true - } else { - featureAvailable = userLogic.values.hasAvailableFeature(featureKey, currentUsage) - } - if (featureAvailable) { - featureAvailableCallback?.() - } else { - actions.showUpgradeModal(featureKey, currentUsage, isGrandfathered) - } - }, setScene: ({ scene, scrollToTop }, _, __, previousState) => { posthog.capture('$pageview') diff --git a/frontend/src/scenes/session-recordings/SessionRecordings.tsx b/frontend/src/scenes/session-recordings/SessionRecordings.tsx index ecf99b63e88b2..383a0f27a5073 100644 --- a/frontend/src/scenes/session-recordings/SessionRecordings.tsx +++ b/frontend/src/scenes/session-recordings/SessionRecordings.tsx @@ -4,6 +4,7 @@ import { useActions, useValues } from 'kea' import { router } from 'kea-router' import { authorizedUrlListLogic, AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' import { PageHeader } from 'lib/components/PageHeader' +import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' import { VersionCheckerBanner } from 'lib/components/VersionChecker/VersionCheckerBanner' import { useAsyncHandler } from 'lib/hooks/useAsyncHandler' import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' @@ -12,7 +13,6 @@ import { LemonTabs } from 'lib/lemon-ui/LemonTabs' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton' -import { sceneLogic } from 'scenes/sceneLogic' import { SceneExport } from 'scenes/sceneTypes' import { sessionRecordingsPlaylistLogic } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' import { teamLogic } from 'scenes/teamLogic' @@ -34,7 +34,7 @@ export function SessionsRecordings(): JSX.Element { const { tab } = useValues(sessionRecordingsLogic) const recordingsDisabled = currentTeam && !currentTeam?.session_recording_opt_in const { reportRecordingPlaylistCreated } = useActions(eventUsageLogic) - const { guardAvailableFeature } = useActions(sceneLogic) + const { guardAvailableFeature } = useValues(upgradeModalLogic) const playlistsLogic = savedSessionRecordingPlaylistsLogic({ tab: ReplayTabs.Recent }) const { playlists } = useValues(playlistsLogic) const { openSettingsPanel } = useActions(sidePanelSettingsLogic) @@ -87,8 +87,7 @@ export function SessionsRecordings(): JSX.Element { ? newPlaylistHandler.onEvent?.(e) : saveFiltersPlaylistHandler.onEvent?.(e) }, - undefined, - playlists.count + { currentUsage: playlists.count } ) } > @@ -111,8 +110,7 @@ export function SessionsRecordings(): JSX.Element { guardAvailableFeature( AvailableFeature.RECORDINGS_PLAYLISTS, () => newPlaylistHandler.onEvent?.(e), - undefined, - playlists.count + { currentUsage: playlists.count } ) } data-attr="save-recordings-playlist-button" diff --git a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx index 7ef7ede4a778e..062b6c5ecf214 100644 --- a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx +++ b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx @@ -1,8 +1,8 @@ import { IconPlus } from '@posthog/icons' -import { useActions, useValues } from 'kea' +import { useValues } from 'kea' +import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { sceneLogic } from 'scenes/sceneLogic' import { AvailableFeature, ReplayTabs } from '~/types' @@ -10,7 +10,7 @@ import { createPlaylist } from '../playlist/playlistUtils' import { savedSessionRecordingPlaylistsLogic } from './savedSessionRecordingPlaylistsLogic' export function SavedSessionRecordingPlaylistsEmptyState(): JSX.Element { - const { guardAvailableFeature } = useActions(sceneLogic) + const { guardAvailableFeature } = useValues(upgradeModalLogic) const playlistsLogic = savedSessionRecordingPlaylistsLogic({ tab: ReplayTabs.Recent }) const { playlists, loadPlaylistsFailed } = useValues(playlistsLogic) return loadPlaylistsFailed ? ( @@ -28,8 +28,7 @@ export function SavedSessionRecordingPlaylistsEmptyState(): JSX.Element { guardAvailableFeature( AvailableFeature.RECORDINGS_PLAYLISTS, () => void createPlaylist({}, true), - undefined, - playlists.count + { currentUsage: playlists.count } ) } > diff --git a/frontend/src/scenes/settings/project/AddMembersModal.tsx b/frontend/src/scenes/settings/project/AddMembersModal.tsx index 1bd544eb04fdd..ea355fa6429e0 100644 --- a/frontend/src/scenes/settings/project/AddMembersModal.tsx +++ b/frontend/src/scenes/settings/project/AddMembersModal.tsx @@ -1,15 +1,15 @@ import { IconPlus } from '@posthog/icons' import { LemonButton, LemonModal, LemonSelect, LemonSelectOption } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' +import { useValues } from 'kea' import { Form } from 'kea-forms' import { RestrictedComponentProps } from 'lib/components/RestrictedArea' +import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' import { TeamMembershipLevel } from 'lib/constants' import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonInputSelect } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect' import { membershipLevelToName, teamMembershipLevelIntegers } from 'lib/utils/permissioning' import { useState } from 'react' -import { sceneLogic } from 'scenes/sceneLogic' import { teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' @@ -20,7 +20,7 @@ import { teamMembersLogic } from './teamMembersLogic' export function AddMembersModalWithButton({ isRestricted }: RestrictedComponentProps): JSX.Element { const { addableMembers, allMembersLoading } = useValues(teamMembersLogic) const { currentTeam } = useValues(teamLogic) - const { guardAvailableFeature } = useActions(sceneLogic) + const { guardAvailableFeature } = useValues(upgradeModalLogic) const { hasAvailableFeature } = useValues(userLogic) const [isVisible, setIsVisible] = useState(false) @@ -35,14 +35,11 @@ export function AddMembersModalWithButton({ isRestricted }: RestrictedComponentP type="primary" data-attr="add-project-members-button" onClick={() => - guardAvailableFeature( - AvailableFeature.PROJECT_BASED_PERMISSIONING, - () => setIsVisible(true), - undefined, - undefined, - !hasAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING) && - currentTeam?.access_control - ) + guardAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING, () => setIsVisible(true), { + isGrandfathered: + !hasAvailableFeature(AvailableFeature.PROJECT_BASED_PERMISSIONING) && + currentTeam?.access_control, + }) } icon={} disabled={isRestricted} diff --git a/frontend/src/scenes/settings/project/ProjectAccessControl.tsx b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx index b250d8386ecc3..fbe81e1942624 100644 --- a/frontend/src/scenes/settings/project/ProjectAccessControl.tsx +++ b/frontend/src/scenes/settings/project/ProjectAccessControl.tsx @@ -2,6 +2,7 @@ import { IconCrown, IconLeave, IconLock, IconUnlock } from '@posthog/icons' import { LemonButton, LemonSelect, LemonSelectOption, LemonSnack, LemonSwitch, LemonTable } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { RestrictedArea, RestrictionScope, useRestrictedArea } from 'lib/components/RestrictedArea' +import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' import { OrganizationMembershipLevel, TeamMembershipLevel } from 'lib/constants' import { IconCancel } from 'lib/lemon-ui/icons' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' @@ -15,7 +16,6 @@ import { teamMembershipLevelIntegers, } from 'lib/utils/permissioning' import { organizationLogic } from 'scenes/organizationLogic' -import { sceneLogic } from 'scenes/sceneLogic' import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' import { userLogic } from 'scenes/userLogic' @@ -208,7 +208,7 @@ export function ProjectAccessControl(): JSX.Element { const { currentOrganization, currentOrganizationLoading } = useValues(organizationLogic) const { currentTeam, currentTeamLoading } = useValues(teamLogic) const { updateCurrentTeam } = useActions(teamLogic) - const { guardAvailableFeature } = useActions(sceneLogic) + const { guardAvailableFeature } = useValues(upgradeModalLogic) const isRestricted = !!useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin, diff --git a/frontend/src/scenes/surveys/SurveyAppearance.tsx b/frontend/src/scenes/surveys/SurveyAppearance.tsx index bc55c6c4b797f..750b880f7d28c 100644 --- a/frontend/src/scenes/surveys/SurveyAppearance.tsx +++ b/frontend/src/scenes/surveys/SurveyAppearance.tsx @@ -1,10 +1,10 @@ import './SurveyAppearance.scss' import { LemonButton, LemonCheckbox, LemonInput, LemonSelect, Link } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' +import { useValues } from 'kea' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' import React, { useEffect, useRef, useState } from 'react' -import { sceneLogic } from 'scenes/sceneLogic' import { AvailableFeature, @@ -138,7 +138,7 @@ export function SurveyAppearance({ export function Customization({ appearance, surveyQuestionItem, onAppearanceChange }: CustomizationProps): JSX.Element { const { surveysStylingAvailable } = useValues(surveysLogic) - const { guardAvailableFeature } = useActions(sceneLogic) + const { guardAvailableFeature } = useValues(upgradeModalLogic) return ( <>
diff --git a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx index 97e7c6afabf40..5240c916f2218 100644 --- a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx +++ b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx @@ -24,7 +24,7 @@ import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { capitalizeFirstLetter, isGroupType, midEllipsis, pluralize } from 'lib/utils' -import { useCallback, useState } from 'react' +import React, { useCallback, useState } from 'react' import { createRoot } from 'react-dom/client' import { InsightErrorState, InsightValidationError } from 'scenes/insights/EmptyStates' import { isOtherBreakdown } from 'scenes/insights/utils' @@ -406,11 +406,10 @@ export function ActorRow({ actor, onOpenRecording, propertiesTimelineFilter }: A
    {matchedRecordings?.length ? matchedRecordings.map((recording, i) => ( - <> + -
  • +
  • { recording.session_id && @@ -431,7 +430,7 @@ export function ActorRow({ actor, onOpenRecording, propertiesTimelineFilter }: A
- + )) : null} diff --git a/frontend/src/toolbar/bar/Toolbar.tsx b/frontend/src/toolbar/bar/Toolbar.tsx index 3bdda9e523e79..f255ccc26800d 100644 --- a/frontend/src/toolbar/bar/Toolbar.tsx +++ b/frontend/src/toolbar/bar/Toolbar.tsx @@ -7,7 +7,7 @@ import { IconLogomark, IconNight, IconQuestion, - IconTarget, + IconSearch, IconToggle, IconX, } from '@posthog/icons' @@ -185,7 +185,7 @@ export function Toolbar(): JSX.Element { /> {isAuthenticated ? ( <> - } menuId="inspect" /> + } menuId="inspect" /> } menuId="heatmap" /> } menuId="actions" /> } menuId="flags" title="Feature flags" /> diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 1193a25f62a08..74d8ca7db1748 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -144,6 +144,9 @@ export enum AvailableFeature { PRODUCT_ANALYTICS_SQL_QUERIES = 'product_analytics_sql_queries', TWOFA_ENFORCEMENT = '2fa_enforcement', AUDIT_LOGS = 'audit_logs', + HIPAA_BAA = 'hipaa_baa', + CUSTOMM_MSA = 'custom_msa', + TWOFA = '2fa', PRIORITY_SUPPORT = 'priority_support', } @@ -1403,7 +1406,7 @@ export interface BillingProductV2Type { unit: string | null unit_amount_usd: string | null plans: BillingV2PlanType[] - contact_support: boolean + contact_support: boolean | null inclusion_only: any features: BillingV2FeatureType[] addons: BillingProductV2AddonType[] @@ -1424,7 +1427,7 @@ export interface BillingProductV2AddonType { subscribed: boolean // sometimes addons are included with the base product, but they aren't subscribed individually included_with_main_product?: boolean - contact_support?: boolean + contact_support: boolean | null unit: string | null unit_amount_usd: string | null current_amount_usd: string | null @@ -1477,10 +1480,10 @@ export interface BillingV2PlanType { product_key: ProductKeyUnion current_plan?: boolean | null tiers?: BillingV2TierType[] | null - unit_amount_usd?: string + unit_amount_usd: string | null included_if?: 'no_active_subscription' | 'has_subscription' | null initial_billing_limit?: number - contact_support?: boolean + contact_support: boolean | null } export interface PlanInterface { diff --git a/package.json b/package.json index f422e9835a6e9..0a268fc208fd5 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@medv/finder": "^3.1.0", "@microlink/react-json-view": "^1.21.3", "@monaco-editor/react": "4.4.6", - "@posthog/icons": "0.6.3", + "@posthog/icons": "0.6.7", "@posthog/plugin-scaffold": "^1.4.4", "@react-hook/size": "^2.1.2", "@rrweb/types": "2.0.0-alpha.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca7351e1a360f..ef81ba7d4c4d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,8 +47,8 @@ dependencies: specifier: 4.4.6 version: 4.4.6(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0) '@posthog/icons': - specifier: 0.6.3 - version: 0.6.3(react-dom@18.2.0)(react@18.2.0) + specifier: 0.6.7 + version: 0.6.7(react-dom@18.2.0)(react@18.2.0) '@posthog/plugin-scaffold': specifier: ^1.4.4 version: 1.4.4 @@ -5180,8 +5180,8 @@ packages: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false - /@posthog/icons@0.6.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-EQ86OFe9omsU9vUCvISksNN+QPH/VHiE4Z0A8FZApSKbiCtsX2zecPgX3ou765V284ktajkeROsrUI0luj8jRw==} + /@posthog/icons@0.6.7(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Jxizmu+fIW6y3kl13oC3avq9YtfRfszmtme75kYFnm+btRGOjwgnTGYPsPCAz9Pw5LsTqii/uNngUsMNotiTZA==} peerDependencies: react: '>=16.14.0' react-dom: '>=16.14.0' diff --git a/posthog/api/authentication.py b/posthog/api/authentication.py index d06e7168d0df2..10538c1d77ceb 100644 --- a/posthog/api/authentication.py +++ b/posthog/api/authentication.py @@ -22,7 +22,6 @@ from rest_framework.exceptions import APIException from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.throttling import UserRateThrottle from sentry_sdk import capture_exception from social_django.views import auth from two_factor.utils import default_device @@ -36,14 +35,11 @@ from posthog.email import is_email_available from posthog.event_usage import report_user_logged_in, report_user_password_reset from posthog.models import OrganizationDomain, User +from posthog.rate_limit import UserPasswordResetThrottle from posthog.tasks.email import send_password_reset from posthog.utils import get_instance_available_sso_providers -class UserPasswordResetThrottle(UserRateThrottle): - rate = "6/day" - - @csrf_protect def logout(request): if request.user.is_authenticated: @@ -183,6 +179,10 @@ def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: """ response = super().create(request, *args, **kwargs) response.status_code = getattr(self, "SUCCESS_STATUS_CODE", status.HTTP_200_OK) + + if response.status_code == status.HTTP_204_NO_CONTENT: + response.data = None + return response @@ -190,6 +190,7 @@ class LoginViewSet(NonCreatingViewSetMixin, viewsets.GenericViewSet): queryset = User.objects.none() serializer_class = LoginSerializer permission_classes = (permissions.AllowAny,) + # NOTE: Throttling is handled by the `axes` package class TwoFactorSerializer(serializers.Serializer): diff --git a/posthog/api/capture.py b/posthog/api/capture.py index 9dfc61aa3979f..73998505bb822 100644 --- a/posthog/api/capture.py +++ b/posthog/api/capture.py @@ -1,37 +1,40 @@ import hashlib import json import re -import time -from datetime import datetime -from typing import Any, Dict, Iterator, List, Optional, Tuple - import structlog +import time +from datetime import datetime, timedelta from dateutil import parser from django.conf import settings from django.http import JsonResponse from django.utils import timezone from django.views.decorators.csrf import csrf_exempt +from enum import Enum from kafka.errors import KafkaError, MessageSizeTooLargeError from kafka.producer.future import FutureRecordMetadata -from prometheus_client import Counter +from prometheus_client import Counter, Gauge from rest_framework import status from sentry_sdk import configure_scope from sentry_sdk.api import capture_exception, start_span from statshog.defaults.django import statsd from token_bucket import Limiter, MemoryStorage +from typing import Any, Dict, Iterator, List, Optional, Tuple, Set from ee.billing.quota_limiting import QuotaLimitingCaches from posthog.api.utils import get_data, get_token, safe_clickhouse_string +from posthog.cache_utils import cache_for from posthog.exceptions import generate_exception_response from posthog.kafka_client.client import KafkaProducer, sessionRecordingKafkaProducer from posthog.kafka_client.topics import ( KAFKA_EVENTS_PLUGIN_INGESTION_HISTORICAL, KAFKA_SESSION_RECORDING_EVENTS, KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS, + KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_OVERFLOW, ) from posthog.logging.timing import timed from posthog.metrics import KLUDGES_COUNTER, LABEL_RESOURCE_TYPE from posthog.models.utils import UUIDT +from posthog.redis import get_client from posthog.session_recordings.session_recording_helpers import ( preprocess_replay_events_for_blob_ingestion, split_replay_events, @@ -85,6 +88,12 @@ labelnames=["reason"], ) +OVERFLOWING_KEYS_LOADED_GAUGE = Gauge( + "capture_overflowing_keys_loaded", + "Number of keys loaded for the overflow redirection, per resource_type.", + labelnames=[LABEL_RESOURCE_TYPE], +) + # This is a heuristic of ids we have seen used as anonymous. As they frequently # have significantly more traffic than non-anonymous distinct_ids, and likely # don't refer to the same underlying person we prefer to partition them randomly @@ -111,6 +120,13 @@ "undefined", } +OVERFLOWING_REDIS_KEY = "@posthog/capture-overflow/" + + +class InputType(Enum): + EVENTS = "events" + REPLAY = "replay" + def build_kafka_event_data( distinct_id: str, @@ -135,7 +151,7 @@ def build_kafka_event_data( } -def _kafka_topic(event_name: str, data: Dict, historical: bool = False) -> str: +def _kafka_topic(event_name: str, historical: bool = False, overflowing: bool = False) -> str: # To allow for different quality of service on session recordings # and other events, we push to a different topic. @@ -143,6 +159,8 @@ def _kafka_topic(event_name: str, data: Dict, historical: bool = False) -> str: case "$snapshot": return KAFKA_SESSION_RECORDING_EVENTS case "$snapshot_items": + if overflowing: + return KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_OVERFLOW return KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS case _: # If the token is in the TOKENS_HISTORICAL_DATA list, we push to the @@ -158,8 +176,9 @@ def log_event( partition_key: Optional[str], headers: Optional[List] = None, historical: bool = False, + overflowing: bool = False, ) -> FutureRecordMetadata: - kafka_topic = _kafka_topic(event_name, data, historical=historical) + kafka_topic = _kafka_topic(event_name, historical=historical, overflowing=overflowing) logger.debug("logging_event", event_name=event_name, kafka_topic=kafka_topic) @@ -297,6 +316,11 @@ def drop_events_over_quota(token: str, events: List[Any]) -> List[Any]: return results +def lib_version_from_query_params(request) -> str: + # url has a ver parameter from posthog-js + return request.GET.get("ver", "unknown") + + @csrf_exempt @timed("posthog_cloud_event_endpoint") def get_event(request): @@ -475,6 +499,8 @@ def get_event(request): try: if replay_events: + lib_version = lib_version_from_query_params(request) + alternative_replay_events = preprocess_replay_events_for_blob_ingestion( replay_events, settings.SESSION_RECORDING_KAFKA_MAX_REQUEST_SIZE_BYTES ) @@ -496,6 +522,7 @@ def get_event(request): sent_at, event_uuid, token, + extra_headers=[("lib_version", lib_version)], ) ) @@ -546,10 +573,24 @@ def parse_event(event): return event -def capture_internal(event, distinct_id, ip, site_url, now, sent_at, event_uuid=None, token=None, historical=False): +def capture_internal( + event, + distinct_id, + ip, + site_url, + now, + sent_at, + event_uuid=None, + token=None, + historical=False, + extra_headers: List[Tuple[str, str]] | None = None, +): if event_uuid is None: event_uuid = UUIDT() + if extra_headers is None: + extra_headers = [] + parsed_event = build_kafka_event_data( distinct_id=distinct_id, ip=ip, @@ -567,11 +608,20 @@ def capture_internal(event, distinct_id, ip, site_url, now, sent_at, event_uuid= kafka_partition_key = None if event["event"] in SESSION_RECORDING_EVENT_NAMES: - kafka_partition_key = event["properties"]["$session_id"] + session_id = event["properties"]["$session_id"] headers = [ ("token", token), - ] - return log_event(parsed_event, event["event"], partition_key=kafka_partition_key, headers=headers) + ] + extra_headers + + overflowing = False + if token in settings.REPLAY_OVERFLOW_FORCED_TOKENS: + overflowing = True + elif settings.REPLAY_OVERFLOW_SESSIONS_ENABLED: + overflowing = session_id in _list_overflowing_keys(InputType.REPLAY) + + return log_event( + parsed_event, event["event"], partition_key=session_id, headers=headers, overflowing=overflowing + ) candidate_partition_key = f"{token}:{distinct_id}" @@ -633,3 +683,19 @@ def is_randomly_partitioned(candidate_partition_key: str) -> bool: keys_to_override = settings.EVENT_PARTITION_KEYS_TO_OVERRIDE return candidate_partition_key in keys_to_override + + +@cache_for(timedelta(seconds=30), background_refresh=True) +def _list_overflowing_keys(input_type: InputType) -> Set[str]: + """Retrieve the active overflows from Redis with caching and pre-fetching + + cache_for will keep the old value if Redis is temporarily unavailable. + In case of a prolonged Redis outage, new pods would fail to retrieve anything and fail + to ingest, but Django is currently unable to start if the common Redis is unhealthy. + Setting REPLAY_OVERFLOW_SESSIONS_ENABLED back to false neutralizes this code path. + """ + now = timezone.now() + redis_client = get_client() + results = redis_client.zrangebyscore(f"{OVERFLOWING_REDIS_KEY}{input_type.value}", min=now.timestamp(), max="+inf") + OVERFLOWING_KEYS_LOADED_GAUGE.labels(input_type.value).set(len(results)) + return {x.decode("utf-8") for x in results} diff --git a/posthog/api/test/__snapshots__/test_cohort.ambr b/posthog/api/test/__snapshots__/test_cohort.ambr index e32cd65cb7367..c7c000f17f8ed 100644 --- a/posthog/api/test/__snapshots__/test_cohort.ambr +++ b/posthog/api/test/__snapshots__/test_cohort.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: TestCohort.test_async_deletion_of_cohort ''' - /* user_id:122 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ + /* user_id:126 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ SELECT count(DISTINCT person_id) FROM cohortpeople WHERE team_id = 2 @@ -11,7 +11,7 @@ # --- # name: TestCohort.test_async_deletion_of_cohort.1 ''' - /* user_id:122 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ + /* user_id:126 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ INSERT INTO cohortpeople SELECT id, 2 as cohort_id, @@ -84,7 +84,7 @@ # --- # name: TestCohort.test_async_deletion_of_cohort.2 ''' - /* user_id:122 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ + /* user_id:126 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ SELECT count(DISTINCT person_id) FROM cohortpeople WHERE team_id = 2 @@ -94,7 +94,7 @@ # --- # name: TestCohort.test_async_deletion_of_cohort.3 ''' - /* user_id:122 celery:posthog.tasks.calculate_cohort.clear_stale_cohort */ + /* user_id:126 celery:posthog.tasks.calculate_cohort.clear_stale_cohort */ SELECT count() FROM cohortpeople WHERE team_id = 2 @@ -104,7 +104,7 @@ # --- # name: TestCohort.test_async_deletion_of_cohort.4 ''' - /* user_id:122 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ + /* user_id:126 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ SELECT count(DISTINCT person_id) FROM cohortpeople WHERE team_id = 2 @@ -114,7 +114,7 @@ # --- # name: TestCohort.test_async_deletion_of_cohort.5 ''' - /* user_id:122 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ + /* user_id:126 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ INSERT INTO cohortpeople SELECT id, 2 as cohort_id, @@ -148,7 +148,7 @@ # --- # name: TestCohort.test_async_deletion_of_cohort.6 ''' - /* user_id:122 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ + /* user_id:126 celery:posthog.tasks.calculate_cohort.calculate_cohort_ch */ SELECT count(DISTINCT person_id) FROM cohortpeople WHERE team_id = 2 @@ -158,7 +158,7 @@ # --- # name: TestCohort.test_async_deletion_of_cohort.7 ''' - /* user_id:122 celery:posthog.tasks.calculate_cohort.clear_stale_cohort */ + /* user_id:126 celery:posthog.tasks.calculate_cohort.clear_stale_cohort */ SELECT count() FROM cohortpeople WHERE team_id = 2 diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index 2d11fc4500367..f38f19faf3f04 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -1739,7 +1739,7 @@ # --- # name: TestFeatureFlag.test_creating_static_cohort.14 ''' - /* user_id:196 celery:posthog.tasks.calculate_cohort.insert_cohort_from_feature_flag */ + /* user_id:200 celery:posthog.tasks.calculate_cohort.insert_cohort_from_feature_flag */ SELECT count(DISTINCT person_id) FROM person_static_cohort WHERE team_id = 2 diff --git a/posthog/api/test/__snapshots__/test_query.ambr b/posthog/api/test/__snapshots__/test_query.ambr index e52c9362b4398..246efec9566f1 100644 --- a/posthog/api/test/__snapshots__/test_query.ambr +++ b/posthog/api/test/__snapshots__/test_query.ambr @@ -157,7 +157,7 @@ # --- # name: TestQuery.test_full_hogql_query_async ''' - /* user_id:463 celery:posthog.tasks.tasks.process_query_task */ + /* user_id:467 celery:posthog.tasks.tasks.process_query_task */ SELECT events.uuid AS uuid, events.event AS event, events.properties AS properties, diff --git a/posthog/api/test/test_authentication.py b/posthog/api/test/test_authentication.py index 3d054e4cb1ac9..ef83517c918c7 100644 --- a/posthog/api/test/test_authentication.py +++ b/posthog/api/test/test_authentication.py @@ -317,6 +317,7 @@ def test_anonymous_user_can_request_password_reset(self, mock_capture): response = self.client.post("/api/reset/", {"email": self.CONFIG_EMAIL}) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.content.decode(), "") + self.assertEqual(response.headers["Content-Length"], "0") user: User = User.objects.get(email=self.CONFIG_EMAIL) self.assertEqual( @@ -434,6 +435,23 @@ def test_cant_reset_more_than_six_times(self): # Three emails should be sent, fourth should not self.assertEqual(len(mail.outbox), 6) + def test_is_rate_limited_on_email_not_ip(self): + set_instance_setting("EMAIL_HOST", "localhost") + + for email in ["email@posthog.com", "other-email@posthog.com"]: + for i in range(7): + with self.settings(CELERY_TASK_ALWAYS_EAGER=True, SITE_URL="https://my.posthog.net"): + response = self.client.post("/api/reset/", {"email": email}) + if i < 6: + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + else: + # Fourth request should fail + self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS) + self.assertDictContainsSubset( + {"attr": None, "code": "throttled", "type": "throttled_error"}, + response.json(), + ) + # Token validation def test_can_validate_token(self): diff --git a/posthog/api/test/test_capture.py b/posthog/api/test/test_capture.py index 4696609ce94ad..2a80186082dea 100644 --- a/posthog/api/test/test_capture.py +++ b/posthog/api/test/test_capture.py @@ -1,21 +1,18 @@ +from collections import Counter +from unittest import mock + import base64 import gzip import json +import lzstring import pathlib +import pytest import random import string +import structlog import zlib -from collections import Counter from datetime import datetime, timedelta from datetime import timezone as tz -from typing import Any, Dict, List, Union, cast -from unittest import mock -from unittest.mock import ANY, MagicMock, call, patch -from urllib.parse import quote - -import lzstring -import pytest -import structlog from django.http import HttpResponse from django.test.client import MULTIPART_CONTENT, Client from django.utils import timezone @@ -27,6 +24,9 @@ from prance import ResolvingParser from rest_framework import status from token_bucket import Limiter, MemoryStorage +from typing import Any, Dict, List, Union, cast +from unittest.mock import ANY, MagicMock, call, patch +from urllib.parse import quote from ee.billing.quota_limiting import QuotaLimitingCaches from posthog.api import capture @@ -41,7 +41,9 @@ from posthog.kafka_client.topics import ( KAFKA_EVENTS_PLUGIN_INGESTION_HISTORICAL, KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS, + KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_OVERFLOW, ) +from posthog.redis import get_client from posthog.settings import ( DATA_UPLOAD_MAX_MEMORY_SIZE, KAFKA_EVENTS_PLUGIN_INGESTION_TOPIC, @@ -230,6 +232,7 @@ def _send_august_2023_version_session_recording_event( distinct_id="ghi789", timestamp=1658516991883, content_type: str | None = None, + query_params: str = "", ) -> HttpResponse: if event_data is None: # event_data is an array of RRWeb events @@ -262,7 +265,7 @@ def _send_august_2023_version_session_recording_event( post_data = {"api_key": self.team.api_token, "data": json.dumps([event for _ in range(number_of_events)])} return self.client.post( - "/s/", + "/s/" + "?" + query_params if query_params else "/s/", data=post_data, content_type=content_type or MULTIPART_CONTENT, ) @@ -1597,6 +1600,65 @@ def test_recording_ingestion_can_write_to_blob_ingestion_topic(self, kafka_produ assert topic_counter == Counter({KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS: 1}) + @patch("posthog.kafka_client.client._KafkaProducer.produce") + def test_recording_ingestion_can_overflow_from_forced_tokens(self, kafka_produce) -> None: + with self.settings( + SESSION_RECORDING_KAFKA_MAX_REQUEST_SIZE_BYTES=20480, + REPLAY_OVERFLOW_FORCED_TOKENS={"another", self.team.api_token}, + REPLAY_OVERFLOW_SESSIONS_ENABLED=False, + ): + self._send_august_2023_version_session_recording_event(event_data=large_data_array) + topic_counter = Counter([call[1]["topic"] for call in kafka_produce.call_args_list]) + + assert topic_counter == Counter({KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_OVERFLOW: 1}) + + @patch("posthog.kafka_client.client._KafkaProducer.produce") + def test_recording_ingestion_can_overflow_from_redis_instructions(self, kafka_produce) -> None: + with self.settings(SESSION_RECORDING_KAFKA_MAX_REQUEST_SIZE_BYTES=20480, REPLAY_OVERFLOW_SESSIONS_ENABLED=True): + redis = get_client() + redis.zadd( + "@posthog/capture-overflow/replay", + { + "overflowing": timezone.now().timestamp() + 1000, + "expired_overflow": timezone.now().timestamp() - 1000, + }, + ) + + # Session is currently overflowing + self._send_august_2023_version_session_recording_event( + event_data=large_data_array, session_id="overflowing" + ) + topic_counter = Counter([call[1]["topic"] for call in kafka_produce.call_args_list]) + assert topic_counter == Counter({KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_OVERFLOW: 1}) + + # This session's entry is expired, data should go to the main topic + kafka_produce.reset_mock() + self._send_august_2023_version_session_recording_event( + event_data=large_data_array, session_id="expired_overflow" + ) + topic_counter = Counter([call[1]["topic"] for call in kafka_produce.call_args_list]) + assert topic_counter == Counter({KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS: 1}) + + @patch("posthog.kafka_client.client._KafkaProducer.produce") + def test_recording_ingestion_ignores_overflow_from_redis_if_disabled(self, kafka_produce) -> None: + with self.settings( + SESSION_RECORDING_KAFKA_MAX_REQUEST_SIZE_BYTES=20480, REPLAY_OVERFLOW_SESSIONS_ENABLED=False + ): + redis = get_client() + redis.zadd( + "@posthog/capture-overflow/replay", + { + "overflowing": timezone.now().timestamp() + 1000, + }, + ) + + # Session is currently overflowing but REPLAY_OVERFLOW_SESSIONS_ENABLED is false + self._send_august_2023_version_session_recording_event( + event_data=large_data_array, session_id="overflowing" + ) + topic_counter = Counter([call[1]["topic"] for call in kafka_produce.call_args_list]) + assert topic_counter == Counter({KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS: 1}) + @patch("posthog.kafka_client.client._KafkaProducer.produce") def test_recording_ingestion_can_write_headers_with_the_message(self, kafka_produce: MagicMock) -> None: with self.settings( @@ -1604,7 +1666,30 @@ def test_recording_ingestion_can_write_headers_with_the_message(self, kafka_prod ): self._send_august_2023_version_session_recording_event() - assert kafka_produce.mock_calls[0].kwargs["headers"] == [("token", "token123")] + assert kafka_produce.mock_calls[0].kwargs["headers"] == [ + ("token", "token123"), + ( + # without setting a version in the URL the default is unknown + "lib_version", + "unknown", + ), + ] + + @patch("posthog.kafka_client.client._KafkaProducer.produce") + def test_recording_ingestion_can_read_version_from_request(self, kafka_produce: MagicMock) -> None: + with self.settings( + SESSION_RECORDING_KAFKA_MAX_REQUEST_SIZE_BYTES=20480, + ): + self._send_august_2023_version_session_recording_event(query_params="ver=1.123.4") + + assert kafka_produce.mock_calls[0].kwargs["headers"] == [ + ("token", "token123"), + ( + # without setting a version in the URL the default is unknown + "lib_version", + "1.123.4", + ), + ] @patch("posthog.kafka_client.client.SessionRecordingKafkaProducer") def test_create_session_recording_kafka_with_expected_hosts( diff --git a/posthog/api/test/test_personal_api_keys.py b/posthog/api/test/test_personal_api_keys.py index afd7b924605a0..1fe9c3af847e9 100644 --- a/posthog/api/test/test_personal_api_keys.py +++ b/posthog/api/test/test_personal_api_keys.py @@ -3,6 +3,7 @@ from rest_framework import status from posthog.jwt import PosthogJwtAudience, encode_jwt +from posthog.models.insight import Insight from posthog.models.organization import Organization from posthog.models.personal_api_key import PersonalAPIKey, hash_key_value from posthog.models.team.team import Team @@ -422,6 +423,23 @@ def test_allows_overriding_write_scopes(self): ) assert response.status_code == status.HTTP_200_OK + def test_works_with_routes_missing_action(self): + insight = Insight.objects.create(team=self.team, name="XYZ", created_by=self.user) + + self.key.scopes = ["sharing_configuration:read"] + self.key.save() + response = self.client.patch( + f"/api/projects/{self.team.id}/insights/{insight.id}/sharing?personal_api_key={self.value}" + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + + self.key.scopes = ["sharing_configuration:write"] + self.key.save() + response = self.client.patch( + f"/api/projects/{self.team.id}/insights/{insight.id}/sharing?personal_api_key={self.value}" + ) + assert response.status_code == status.HTTP_200_OK + class TestPersonalAPIKeysWithOrganizationScopeAPIAuthentication(PersonalAPIKeysBaseTest): def setUp(self): diff --git a/posthog/api/user.py b/posthog/api/user.py index 7e72d46b88cb8..28b4a42b8620a 100644 --- a/posthog/api/user.py +++ b/posthog/api/user.py @@ -23,7 +23,7 @@ from rest_framework.exceptions import NotFound from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response -from rest_framework.throttling import UserRateThrottle + from two_factor.forms import TOTPDeviceForm from two_factor.utils import default_device @@ -46,6 +46,7 @@ from posthog.models.organization import Organization from posthog.models.user import NOTIFICATION_DEFAULTS, Notifications from posthog.permissions import APIScopePermission +from posthog.rate_limit import UserAuthenticationThrottle, UserEmailVerificationThrottle from posthog.tasks import user_identify from posthog.tasks.email import send_email_change_emails from posthog.user_permissions import UserPermissions @@ -53,20 +54,6 @@ from posthog.constants import PERMITTED_FORUM_DOMAINS -class UserAuthenticationThrottle(UserRateThrottle): - rate = "5/minute" - - def allow_request(self, request, view): - # only throttle non-GET requests - if request.method == "GET": - return True - return super().allow_request(request, view) - - -class UserEmailVerificationThrottle(UserRateThrottle): - rate = "6/day" - - class ScenePersonalisationBasicSerializer(serializers.ModelSerializer): class Meta: model = UserScenePersonalisation diff --git a/posthog/batch_exports/models.py b/posthog/batch_exports/models.py index 35fa6e8ba4754..70b85c4d35bde 100644 --- a/posthog/batch_exports/models.py +++ b/posthog/batch_exports/models.py @@ -306,5 +306,5 @@ class Status(models.TextChoices): def workflow_id(self) -> str: """Return the Workflow id that corresponds to this BatchExportBackfill model.""" start_at = self.start_at.strftime("%Y-%m-%dT%H:%M:%S") - end_at = self.end_at.strftime("%Y-%m-%dT%H:%M:%S") + end_at = self.end_at and self.end_at.strftime("%Y-%m-%dT%H:%M:%S") return f"{self.batch_export.id}-Backfill-{start_at}-{end_at}" diff --git a/posthog/clickhouse/migrations/0055_add_minmax_index_on_inserted_at.py b/posthog/clickhouse/migrations/0055_add_minmax_index_on_inserted_at.py new file mode 100644 index 0000000000000..a1458a2b4a391 --- /dev/null +++ b/posthog/clickhouse/migrations/0055_add_minmax_index_on_inserted_at.py @@ -0,0 +1,7 @@ +from posthog.clickhouse.client.migration_tools import run_sql_with_exceptions +from posthog.models.event.sql import EVENTS_TABLE_INSERTED_AT_INDEX_SQL, EVENTS_TABLE_MATERIALIZE_INSERTED_AT_INDEX_SQL + +operations = [ + run_sql_with_exceptions(EVENTS_TABLE_INSERTED_AT_INDEX_SQL), + run_sql_with_exceptions(EVENTS_TABLE_MATERIALIZE_INSERTED_AT_INDEX_SQL), +] diff --git a/posthog/hogql/test/test_property.py b/posthog/hogql/test/test_property.py index f271ee5e2f4ff..c50615eb6730a 100644 --- a/posthog/hogql/test/test_property.py +++ b/posthog/hogql/test/test_property.py @@ -431,37 +431,39 @@ def test_tag_name_to_expr(self): def test_selector_to_expr(self): self.assertEqual( self._selector_to_expr("div"), - clear_locations(elements_chain_match('div([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))')), + clear_locations(elements_chain_match('(^|;)div([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))')), ) self.assertEqual( self._selector_to_expr("div > div"), clear_locations( elements_chain_match( - 'div([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))div([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s))).*' + '(^|;)div([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))div([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s))).*' ) ), ) self.assertEqual( self._selector_to_expr("a[href='boo']"), clear_locations( - elements_chain_match('a.*?href="boo".*?([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))') + elements_chain_match('(^|;)a.*?href="boo".*?([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))') ), ) self.assertEqual( self._selector_to_expr(".class"), - clear_locations(elements_chain_match('.*?\\.class([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))')), + clear_locations( + elements_chain_match('(^|;).*?\\.class([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))') + ), ) self.assertEqual( self._selector_to_expr("#withid"), clear_locations( - elements_chain_match('.*?attr_id="withid".*?([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))') + elements_chain_match('(^|;).*?attr_id="withid".*?([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))') ), ) self.assertEqual( self._selector_to_expr("#with-dashed-id"), clear_locations( elements_chain_match( - '.*?attr_id="with\\-dashed\\-id".*?([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))' + '(^|;).*?attr_id="with\\-dashed\\-id".*?([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))' ) ), ) @@ -473,7 +475,7 @@ def test_selector_to_expr(self): self._selector_to_expr("#with\\slashed\\id"), clear_locations( elements_chain_match( - '.*?attr_id="with\\\\slashed\\\\id".*?([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))' + '(^|;).*?attr_id="with\\\\slashed\\\\id".*?([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))' ) ), ) @@ -526,7 +528,7 @@ def test_action_to_expr(self): "event = '$autocapture' and elements_chain =~ {regex1} and elements_chain =~ {regex2}", { "regex1": ast.Constant( - value='a.*?\\.active\\..*?nav\\-link([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))' + value='(^|;)a.*?\\.active\\..*?nav\\-link([-_a-zA-Z0-9\\.:"= ]*?)?($|;|:([^;^\\s]*(;|$|\\s)))' ), "regex2": ast.Constant(value="(^|;)a(\\.|$|;|:)"), }, diff --git a/posthog/hogql_queries/actors_query_runner.py b/posthog/hogql_queries/actors_query_runner.py index 1e714a3a2c0a0..93e9f40c70739 100644 --- a/posthog/hogql_queries/actors_query_runner.py +++ b/posthog/hogql_queries/actors_query_runner.py @@ -42,7 +42,7 @@ def determine_strategy(self) -> ActorStrategy: def get_recordings(self, event_results, recordings_lookup) -> Generator[dict, None, None]: return ( {"session_id": session_id, "events": recordings_lookup[session_id]} - for session_id in (event[2] for event in event_results) + for session_id in set(event[2] for event in event_results) if session_id in recordings_lookup ) @@ -66,7 +66,7 @@ def enrich_with_actors( yield new_row def prepare_recordings(self, column_name, input_columns): - if column_name != "person" or "matched_recordings" not in input_columns: + if (column_name != "person" and column_name != "actor") or "matched_recordings" not in input_columns: return None, None column_index_events = input_columns.index("matched_recordings") diff --git a/posthog/kafka_client/topics.py b/posthog/kafka_client/topics.py index c15b6024be4cd..27e1ce307deae 100644 --- a/posthog/kafka_client/topics.py +++ b/posthog/kafka_client/topics.py @@ -24,6 +24,8 @@ KAFKA_SESSION_RECORDING_EVENTS = f"{KAFKA_PREFIX}session_recording_events{SUFFIX}" # from capture to recordings blob ingestion consumer KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_EVENTS = f"{KAFKA_PREFIX}session_recording_snapshot_item_events{SUFFIX}" +KAFKA_SESSION_RECORDING_SNAPSHOT_ITEM_OVERFLOW = f"{KAFKA_PREFIX}session_recording_snapshot_item_overflow{SUFFIX}" + # from recordings consumer to clickhouse KAFKA_CLICKHOUSE_SESSION_REPLAY_EVENTS = f"{KAFKA_PREFIX}clickhouse_session_replay_events{SUFFIX}" KAFKA_CLICKHOUSE_SESSION_RECORDING_EVENTS = f"{KAFKA_PREFIX}clickhouse_session_recording_events{SUFFIX}" diff --git a/posthog/management/commands/backfill_distinct_id_overrides.py b/posthog/management/commands/backfill_distinct_id_overrides.py index 35133ef4addbf..507e744a93d0e 100644 --- a/posthog/management/commands/backfill_distinct_id_overrides.py +++ b/posthog/management/commands/backfill_distinct_id_overrides.py @@ -2,6 +2,7 @@ import logging from dataclasses import dataclass +from typing import Sequence import structlog from django.core.management.base import BaseCommand, CommandError @@ -14,10 +15,12 @@ @dataclass -class BackfillQuery: +class Backfill: team_id: int def execute(self, dry_run: bool = False) -> None: + logger.info("Starting %r...", self) + query = """ SELECT team_id, @@ -51,20 +54,54 @@ def execute(self, dry_run: bool = False) -> None: parameters, ) + # XXX: The RETURNING set isn't really useful here, but this QuerySet + # needs to be iterated over to force execution, so we might as well + # return something... + updated_teams = list( + Team.objects.raw( + """ + UPDATE posthog_team + SET extra_settings = COALESCE(extra_settings, '{}'::jsonb) || '{"distinct_id_overrides_backfilled": true}'::jsonb + WHERE id = %s + RETURNING * + """, + [self.team_id], + ) + ) + assert not len(updated_teams) > 1 + + logger.info("Completed %r!", self) + class Command(BaseCommand): help = "Backfill person_distinct_id_overrides records." def add_arguments(self, parser): - parser.add_argument("--team-id", required=True, type=int, help="team to backfill for") + parser.add_argument( + "--team-id", + required=False, + type=int, + dest="team_id_list", + action="append", + help="team(s) to backfill (defaults to all un-backfilled teams)", + ) parser.add_argument( "--live-run", action="store_true", help="actually execute INSERT queries (default is dry-run)" ) - def handle(self, *, live_run: bool, team_id: int, **options): + def handle(self, *, live_run: bool, team_id_list: Sequence[int] | None, **options): logger.setLevel(logging.INFO) - if not Team.objects.filter(id=team_id).exists(): - raise CommandError(f"Team with id={team_id!r} does not exist") + if team_id_list is not None: + team_ids = set(team_id_list) + existing_team_ids = set(Team.objects.filter(id__in=team_ids).values_list("id", flat=True)) + if existing_team_ids != team_ids: + raise CommandError(f"Teams with ids {team_ids - existing_team_ids!r} do not exist") + else: + team_ids = set( + Team.objects.exclude(extra_settings__distinct_id_overrides_backfilled=True).values_list("id", flat=True) + ) - BackfillQuery(team_id).execute(dry_run=not live_run) + logger.info("Starting backfill for %s teams...", len(team_ids)) + for team_id in team_ids: + Backfill(team_id).execute(dry_run=not live_run) diff --git a/posthog/management/commands/test/test_backfill_distinct_id_overrides.py b/posthog/management/commands/test/test_backfill_distinct_id_overrides.py index 6a161d6205d18..55f7c76116936 100644 --- a/posthog/management/commands/test/test_backfill_distinct_id_overrides.py +++ b/posthog/management/commands/test/test_backfill_distinct_id_overrides.py @@ -2,7 +2,7 @@ import uuid from posthog.clickhouse.client.execute import sync_execute -from posthog.management.commands.backfill_distinct_id_overrides import BackfillQuery +from posthog.management.commands.backfill_distinct_id_overrides import Backfill from posthog.test.base import BaseTest, ClickhouseTestMixin @@ -32,7 +32,7 @@ def __run_test_backfill(self, dry_run: bool) -> None: {"team_id": self.team.id}, ) == [(0,)] - BackfillQuery(self.team.id).execute(dry_run=dry_run) + Backfill(self.team.id).execute(dry_run=dry_run) read_columns = ["team_id", "distinct_id", "person_id", "version"] distinct_id_override_rows = sync_execute( diff --git a/posthog/models/event/sql.py b/posthog/models/event/sql.py index 410904ba006d4..8214ac90fdce0 100644 --- a/posthog/models/event/sql.py +++ b/posthog/models/event/sql.py @@ -106,6 +106,18 @@ storage_policy=STORAGE_POLICY(), ) +EVENTS_TABLE_INSERTED_AT_INDEX_SQL = """ +ALTER TABLE {table_name} ON CLUSTER {cluster} +ADD INDEX `minmax_inserted_at` COALESCE(`inserted_at`, `_timestamp`) +TYPE minmax +GRANULARITY 1 +""".format(table_name=EVENTS_DATA_TABLE(), cluster=settings.CLICKHOUSE_CLUSTER) + +EVENTS_TABLE_MATERIALIZE_INSERTED_AT_INDEX_SQL = """ +ALTER TABLE {table_name} ON CLUSTER {cluster} +MATERIALIZE INDEX `minmax_inserted_at` +""".format(table_name=EVENTS_DATA_TABLE(), cluster=settings.CLICKHOUSE_CLUSTER) + # we add the settings to prevent poison pills from stopping ingestion # kafka_skip_broken_messages is an int, not a boolean, so we explicitly set # the max block size to consume from kafka such that we skip _all_ broken messages diff --git a/posthog/models/property/util.py b/posthog/models/property/util.py index bd684283049c7..cae1be3340eac 100644 --- a/posthog/models/property/util.py +++ b/posthog/models/property/util.py @@ -847,21 +847,24 @@ def process_ok_values(ok_values: Any, operator: OperatorType) -> List[str]: def build_selector_regex(selector: Selector) -> str: regex = r"" for tag in selector.parts: - if tag.data.get("tag_name") and isinstance(tag.data["tag_name"], str): - if tag.data["tag_name"] == "*": - regex += ".+" - else: - regex += re.escape(tag.data["tag_name"]) + if tag.data.get("tag_name") and isinstance(tag.data["tag_name"], str) and tag.data["tag_name"] != "*": + # The elements in the elements_chain are separated by the semicolon + regex += re.escape(tag.data["tag_name"]) if tag.data.get("attr_class__contains"): - regex += r".*?\.{}".format(r"\..*?".join([re.escape(s) for s in sorted(tag.data["attr_class__contains"])])) + regex += r".*?\." + r"\..*?".join([re.escape(s) for s in sorted(tag.data["attr_class__contains"])]) if tag.ch_attributes: - regex += ".*?" + regex += r".*?" for key, value in sorted(tag.ch_attributes.items()): - regex += '{}="{}".*?'.format(re.escape(key), re.escape(str(value))) + regex += rf'{re.escape(key)}="{re.escape(str(value))}".*?' regex += r'([-_a-zA-Z0-9\.:"= ]*?)?($|;|:([^;^\s]*(;|$|\s)))' if tag.direct_descendant: - regex += ".*" - return regex + regex += r".*" + if regex: + # Always start matching at the beginning of an element in the chain string + # This is to avoid issues like matching elements with class "foo" when looking for elements with tag name "foo" + return r"(^|;)" + regex + else: + return r"" class HogQLPropertyChecker(TraversingVisitor): diff --git a/posthog/models/test/test_event_model.py b/posthog/models/test/test_event_model.py index e918291ca15e4..cbe6a2bcad70c 100644 --- a/posthog/models/test/test_event_model.py +++ b/posthog/models/test/test_event_model.py @@ -72,8 +72,15 @@ def _setup_action_selector_events(self): attr_class=["one-class"], ), Element(tag_name="button", nth_child=0, nth_of_type=0), - Element(tag_name="div", nth_child=0, nth_of_type=0), - Element(tag_name="div", nth_child=0, nth_of_type=0, attr_id="nested"), + Element( + # Important that in this hierarchy the div is sandwiched between button and section. + # This way makes sure that any conditions which should match this element also work + # if the element is neither first nor last in the hierarchy. + tag_name="div", + nth_child=0, + nth_of_type=0, + ), + Element(tag_name="section", nth_child=0, nth_of_type=0, attr_id="nested"), ], ) @@ -417,6 +424,37 @@ def test_with_class_with_escaped_slashes(self): self.assertEqual(events[0].uuid, event1_uuid) self.assertEqual(len(events), 1) + def test_with_tag_matching_class_selector(self): + _create_person(distinct_ids=["whatever"], team=self.team) + action1 = Action.objects.create(team=self.team) + ActionStep.objects.create( + event="$autocapture", + action=action1, + selector="input", # This should ONLY match the tag, but not a class named `input` + ) + event_matching_tag_uuid = _create_event( + event="$autocapture", + team=self.team, + distinct_id="whatever", + elements=[ + Element(tag_name="span", attr_class=None), + Element(tag_name="input", attr_class=["button"]), # Should match + ], + ) + _create_event( + event="$autocapture", + team=self.team, + distinct_id="whatever", + elements=[ + Element(tag_name="span", attr_class=None), + Element(tag_name="button", attr_class=["input"]), # Cannot match + ], + ) + + events = _get_events_for_action(action1) + self.assertEqual(len(events), 1) + self.assertEqual(events[0].uuid, event_matching_tag_uuid) + def test_attributes(self): _create_person(distinct_ids=["whatever"], team=self.team) event1_uuid = _create_event( diff --git a/posthog/permissions.py b/posthog/permissions.py index 4d42f165020bf..0d61b015d7f84 100644 --- a/posthog/permissions.py +++ b/posthog/permissions.py @@ -268,11 +268,19 @@ class APIScopePermission(BasePermission): """ - write_actions: list[str] = ["create", "update", "partial_update", "destroy"] + write_actions: list[str] = ["create", "update", "partial_update", "patch", "destroy"] read_actions: list[str] = ["list", "retrieve"] scope_object_read_actions: list[str] = [] scope_object_write_actions: list[str] = [] + def _get_action(self, request, view) -> str: + # TRICKY: DRF doesn't have an action for non-detail level "patch" calls which we use sometimes + + if not view.action: + if request.method == "PATCH" and not view.detail: + return "patch" + return view.action + def has_permission(self, request, view) -> bool: # NOTE: We do this first to error out quickly if the view is missing the required attribute # Helps devs remember to add it. @@ -341,12 +349,13 @@ def get_required_scopes(self, request, view) -> list[str]: if scope_object == "INTERNAL": raise PermissionDenied(f"This action does not support Personal API Key access") + action = self._get_action(request, view) read_actions = getattr(view, "scope_object_read_actions", self.read_actions) write_actions = getattr(view, "scope_object_write_actions", self.write_actions) - if view.action in write_actions: + if action in write_actions: return [f"{scope_object}:write"] - elif view.action in read_actions or request.method == "OPTIONS": + elif action in read_actions or request.method == "OPTIONS": return [f"{scope_object}:read"] # If we get here this typically means an action was called without a required scope diff --git a/posthog/rate_limit.py b/posthog/rate_limit.py index dbaa478d9f462..856d1b6cceb32 100644 --- a/posthog/rate_limit.py +++ b/posthog/rate_limit.py @@ -1,3 +1,4 @@ +import hashlib import re import time from functools import lru_cache @@ -222,6 +223,26 @@ def get_bucket_key(self, request): return ident +class UserOrEmailRateThrottle(SimpleRateThrottle): + """ + Typically throttling is on the user or the IP address. + For unauthenticated signup/login requests we want to throttle on the email address. + """ + + scope = "user" + + def get_cache_key(self, request, view): + if request.user and request.user.is_authenticated: + ident = request.user.pk + else: + # For unauthenticated requests, we want to throttle on something unique to the user they are trying to work with + # This could be email for example when logging in or uuid when verifying email + ident = request.data.get("email") or request.data.get("uuid") or self.get_ident(request) + ident = hashlib.sha256(ident.encode()).hexdigest() + + return self.cache_format % {"scope": self.scope, "ident": ident} + + class BurstRateThrottle(TeamRateThrottle): # Throttle class that's applied on all endpoints (except for capture + decide) # Intended to block quick bursts of requests, per project @@ -262,3 +283,24 @@ class AISustainedRateThrottle(UserRateThrottle): # Intended to block slower but sustained bursts of requests, per user scope = "ai_sustained" rate = "40/day" + + +class UserPasswordResetThrottle(UserOrEmailRateThrottle): + scope = "user_password_reset" + rate = "6/day" + + +class UserAuthenticationThrottle(UserOrEmailRateThrottle): + scope = "user_authentication" + rate = "5/minute" + + def allow_request(self, request, view): + # only throttle non-GET requests + if request.method == "GET": + return True + return super().allow_request(request, view) + + +class UserEmailVerificationThrottle(UserOrEmailRateThrottle): + scope = "user_email_verification" + rate = "6/day" diff --git a/posthog/settings/ingestion.py b/posthog/settings/ingestion.py index ce0882aeb439f..2bc532ac9cd92 100644 --- a/posthog/settings/ingestion.py +++ b/posthog/settings/ingestion.py @@ -1,5 +1,4 @@ import os - import structlog from posthog.settings.utils import get_from_env, get_list, get_set @@ -23,6 +22,8 @@ QUOTA_LIMITING_ENABLED = get_from_env("QUOTA_LIMITING_ENABLED", False, type_cast=str_to_bool) +# Capture-side overflow detection for analytics events. +# Not accurate enough, superseded by detection in plugin-server and should be phased out. PARTITION_KEY_AUTOMATIC_OVERRIDE_ENABLED = get_from_env( "PARTITION_KEY_AUTOMATIC_OVERRIDE_ENABLED", type_cast=bool, default=False ) @@ -31,6 +32,10 @@ "PARTITION_KEY_BUCKET_REPLENTISH_RATE", type_cast=float, default=1.0 ) +# Overflow configuration for session replay +REPLAY_OVERFLOW_FORCED_TOKENS = get_set(os.getenv("REPLAY_OVERFLOW_FORCED_TOKENS", "")) +REPLAY_OVERFLOW_SESSIONS_ENABLED = get_from_env("REPLAY_OVERFLOW_SESSIONS_ENABLED", type_cast=bool, default=False) + REPLAY_RETENTION_DAYS_MIN = get_from_env("REPLAY_RETENTION_DAYS_MIN", type_cast=int, default=30) REPLAY_RETENTION_DAYS_MAX = get_from_env("REPLAY_RETENTION_DAYS_MAX", type_cast=int, default=90) diff --git a/posthog/temporal/data_imports/__init__.py b/posthog/temporal/data_imports/__init__.py index c6a142c712d39..e4d5887f22d15 100644 --- a/posthog/temporal/data_imports/__init__.py +++ b/posthog/temporal/data_imports/__init__.py @@ -1,10 +1,10 @@ from posthog.temporal.data_imports.external_data_job import ( ExternalDataJobWorkflow, create_external_data_job_model, - update_external_data_job_model, + create_source_templates, run_external_data_job, + update_external_data_job_model, validate_schema_activity, - create_source_templates, ) WORKFLOWS = [ExternalDataJobWorkflow] diff --git a/unit.json b/unit.json index 3982169eec719..72e3d2f03edb6 100644 --- a/unit.json +++ b/unit.json @@ -19,11 +19,7 @@ "posthog": [ { "match": { - "uri": [ - "/_health", - "/_readyz", - "/_livez" - ] + "uri": ["/_health", "/_readyz", "/_livez"] }, "action": { "pass": "applications/posthog-health"