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/events.cy.ts b/cypress/e2e/events.cy.ts index d8fea2656987c..fab6627b467d0 100644 --- a/cypress/e2e/events.cy.ts +++ b/cypress/e2e/events.cy.ts @@ -72,7 +72,7 @@ describe('Events', () => { cy.get('[data-attr="new-prop-filter-EventPropertyFilters.0"]').click() cy.get('[data-attr=taxonomic-filter-searchfield]').click() cy.get('[data-attr=prop-filter-event_properties-0]').click() - cy.get('[data-attr=prop-val] .ant-select-selector').click({ force: true }) + cy.get('[data-attr=prop-val] .LemonInput').click({ force: true }) cy.wait('@getBrowserValues').then(() => { cy.get('[data-attr=prop-val-0]').click() cy.get('.DataTable').should('exist') 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/e2e/surveys.cy.ts b/cypress/e2e/surveys.cy.ts index 7d72c850c7ac4..e3efcd4c48a0f 100644 --- a/cypress/e2e/surveys.cy.ts +++ b/cypress/e2e/surveys.cy.ts @@ -94,7 +94,7 @@ describe('Surveys', () => { // select the first property cy.get('[data-attr="property-select-toggle-0"]').click() cy.get('[data-attr="prop-filter-person_properties-0"]').click() - cy.get('[data-attr=prop-val] .ant-select-selector').click({ force: true }) + cy.get('[data-attr=prop-val] .LemonInput').click({ force: true }) cy.get('[data-attr=prop-val-0]').click({ force: true }) cy.get('[data-attr="rollout-percentage"]').type('100') 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/cypress/productAnalytics/index.ts b/cypress/productAnalytics/index.ts index 00b9279410e1b..6fd8a5c521de5 100644 --- a/cypress/productAnalytics/index.ts +++ b/cypress/productAnalytics/index.ts @@ -215,14 +215,6 @@ export const dashboard = { cy.get('[data-attr="prop-val-0"]').click({ force: true }) cy.get('.PropertyFilterButton').should('have.length', 1) }, - addPropertyFilter(type: string = 'Browser', value: string = 'Chrome'): void { - cy.get('.PropertyFilterButton').should('have.length', 0) - cy.get('[data-attr="property-filter-0"]').click() - cy.get('[data-attr="taxonomic-filter-searchfield"]').click().type('Browser').wait(1000) - cy.get('[data-attr="prop-filter-event_properties-0"]').click({ force: true }) - cy.get('.ant-select-selector').type(value) - cy.get('.ant-select-item-option-content').click({ force: true }) - }, } export function createInsight(insightName: string): void { diff --git a/docker-compose.base.yml b/docker-compose.base.yml index 2e5ce0c2bdb9a..073af7c1d04bb 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -103,7 +103,6 @@ services: KAFKA_HOSTS: 'kafka:9092' REDIS_URL: 'redis://redis:6379/' - plugins: command: ./bin/plugin-server --no-restart-loop restart: on-failure @@ -152,8 +151,6 @@ services: volumes: - /var/lib/elasticsearch/data temporal: - - environment: - DB=postgresql - DB_PORT=5432 @@ -190,4 +187,3 @@ services: restart: on-failure environment: TEMPORAL_HOST: temporal - diff --git a/docker-compose.dev-full.yml b/docker-compose.dev-full.yml index ba940322fb3dd..b8dbe9ebd3c7e 100644 --- a/docker-compose.dev-full.yml +++ b/docker-compose.dev-full.yml @@ -182,4 +182,4 @@ services: - clickhouse - kafka - object_storage - - temporal \ No newline at end of file + - temporal diff --git a/frontend/@posthog/lemon-ui/src/index.ts b/frontend/@posthog/lemon-ui/src/index.ts index bd2d23a5b74ea..dd674ab2c4e30 100644 --- a/frontend/@posthog/lemon-ui/src/index.ts +++ b/frontend/@posthog/lemon-ui/src/index.ts @@ -21,7 +21,7 @@ export * from 'lib/lemon-ui/LemonModal' export * from 'lib/lemon-ui/LemonRow' export * from 'lib/lemon-ui/LemonSegmentedButton' export * from 'lib/lemon-ui/LemonSelect' -export * from 'lib/lemon-ui/LemonSelectMultiple' +export * from 'lib/lemon-ui/LemonInputSelect' export * from 'lib/lemon-ui/LemonSkeleton' export * from 'lib/lemon-ui/LemonSnack' export * from 'lib/lemon-ui/LemonSwitch' diff --git a/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--dark.png b/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--dark.png index f10afc853072e..f97d12fe86644 100644 Binary files a/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--dark.png and b/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--dark.png differ diff --git a/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--light.png b/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--light.png index 61b90277049e8..418f247258351 100644 Binary files a/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--light.png and b/frontend/__snapshots__/filters-cohort-filters-fields-person-properties--basic--light.png differ diff --git a/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters--dark.png b/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters--dark.png index 6be95c84a2b94..78d03d3f2878f 100644 Binary files a/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters--dark.png and b/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters--dark.png differ diff --git a/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters--light.png b/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters--light.png index d56fbb0049291..c85b6df7b3923 100644 Binary files a/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters--light.png and b/frontend/__snapshots__/filters-propertyfilters--comparing-property-filters--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-colors--color-palette--dark.png b/frontend/__snapshots__/lemon-ui-colors--color-palette--dark.png index 9b77208d77294..74150138e8bd3 100644 Binary files a/frontend/__snapshots__/lemon-ui-colors--color-palette--dark.png and b/frontend/__snapshots__/lemon-ui-colors--color-palette--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-colors--color-palette--light.png b/frontend/__snapshots__/lemon-ui-colors--color-palette--light.png index 48775cfc8bad6..62cf1c2370dad 100644 Binary files a/frontend/__snapshots__/lemon-ui-colors--color-palette--light.png and b/frontend/__snapshots__/lemon-ui-colors--color-palette--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--default--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--default--dark.png new file mode 100644 index 0000000000000..96cc9e2c65fb9 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--default--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--default--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--default--light.png new file mode 100644 index 0000000000000..7ff297a125ea3 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--default--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--disabled--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--disabled--dark.png new file mode 100644 index 0000000000000..109cf0f8db9be Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--disabled--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--disabled--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--disabled--light.png new file mode 100644 index 0000000000000..9baed9618a6aa Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--disabled--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--loading--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--loading--dark.png new file mode 100644 index 0000000000000..004699f0e3761 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--loading--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--loading--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--loading--light.png new file mode 100644 index 0000000000000..5bf9eb654b451 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--loading--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select--dark.png new file mode 100644 index 0000000000000..dfcba89cef491 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select--light.png new file mode 100644 index 0000000000000..6f9828363b7d4 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--dark.png new file mode 100644 index 0000000000000..aa1ba92d1702f Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--light.png new file mode 100644 index 0000000000000..bce77b83e7476 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--no-options--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--no-options--dark.png new file mode 100644 index 0000000000000..19881c003fc27 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--no-options--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--no-options--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--no-options--light.png new file mode 100644 index 0000000000000..a128f92384aa1 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--no-options--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--prefilled-many-values--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--prefilled-many-values--dark.png new file mode 100644 index 0000000000000..52f7374e18bb6 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--prefilled-many-values--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--prefilled-many-values--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--prefilled-many-values--light.png new file mode 100644 index 0000000000000..9014b3540f2f4 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--prefilled-many-values--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--single-option-with-custom--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--single-option-with-custom--dark.png new file mode 100644 index 0000000000000..349c97229174d Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--single-option-with-custom--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--single-option-with-custom--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--single-option-with-custom--light.png new file mode 100644 index 0000000000000..9ab0f7424fad8 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-input-select--single-option-with-custom--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-slider--basic--dark.png b/frontend/__snapshots__/lemon-ui-lemon-slider--basic--dark.png index 58a3777e5b1bc..b1200cf4c527d 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-slider--basic--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-slider--basic--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-slider--basic--light.png b/frontend/__snapshots__/lemon-ui-lemon-slider--basic--light.png index 828e365954235..787e8259d52c3 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-slider--basic--light.png and b/frontend/__snapshots__/lemon-ui-lemon-slider--basic--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--dark.png b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--dark.png index 3b4b55227faac..fcfe5de2715ed 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--light.png b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--light.png index 44c5df9cd7035..2ebc9ce11d8dc 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--light.png and b/frontend/__snapshots__/lemon-ui-lemon-tag--breakdown-tag--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-scrollable-shadows--horizontal--dark.png b/frontend/__snapshots__/lemon-ui-scrollable-shadows--horizontal--dark.png index 80fd7103af84e..a513619924cf3 100644 Binary files a/frontend/__snapshots__/lemon-ui-scrollable-shadows--horizontal--dark.png and b/frontend/__snapshots__/lemon-ui-scrollable-shadows--horizontal--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-scrollable-shadows--horizontal--light.png b/frontend/__snapshots__/lemon-ui-scrollable-shadows--horizontal--light.png index ca420ed8bdc54..f90ea838ab39e 100644 Binary files a/frontend/__snapshots__/lemon-ui-scrollable-shadows--horizontal--light.png and b/frontend/__snapshots__/lemon-ui-scrollable-shadows--horizontal--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-scrollable-shadows--vertical--dark.png b/frontend/__snapshots__/lemon-ui-scrollable-shadows--vertical--dark.png index ea2b6db0a77ea..b7de7c83d8f1f 100644 Binary files a/frontend/__snapshots__/lemon-ui-scrollable-shadows--vertical--dark.png and b/frontend/__snapshots__/lemon-ui-scrollable-shadows--vertical--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-scrollable-shadows--vertical--light.png b/frontend/__snapshots__/lemon-ui-scrollable-shadows--vertical--light.png index da9075401372e..8725eaaeaea92 100644 Binary files a/frontend/__snapshots__/lemon-ui-scrollable-shadows--vertical--light.png and b/frontend/__snapshots__/lemon-ui-scrollable-shadows--vertical--light.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--navigation-base--dark.png b/frontend/__snapshots__/posthog-3000-navigation--navigation-base--dark.png index 5ab6b69cf8edc..06adea217229e 100644 Binary files a/frontend/__snapshots__/posthog-3000-navigation--navigation-base--dark.png and b/frontend/__snapshots__/posthog-3000-navigation--navigation-base--dark.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--navigation-base--light.png b/frontend/__snapshots__/posthog-3000-navigation--navigation-base--light.png index 8252b5fdf1bb7..d1bd6fb608eaa 100644 Binary files a/frontend/__snapshots__/posthog-3000-navigation--navigation-base--light.png and b/frontend/__snapshots__/posthog-3000-navigation--navigation-base--light.png differ diff --git a/frontend/__snapshots__/scenes-app-batchexports--view-export--dark.png b/frontend/__snapshots__/scenes-app-batchexports--view-export--dark.png index 0f8b113af43cb..5090a2518e09f 100644 Binary files a/frontend/__snapshots__/scenes-app-batchexports--view-export--dark.png and b/frontend/__snapshots__/scenes-app-batchexports--view-export--dark.png differ 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-app-notebooks--notebooks-list--dark.png b/frontend/__snapshots__/scenes-app-notebooks--notebooks-list--dark.png index 06adf374409a7..5c5c43c394a5d 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--notebooks-list--dark.png and b/frontend/__snapshots__/scenes-app-notebooks--notebooks-list--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--notebooks-list--light.png b/frontend/__snapshots__/scenes-app-notebooks--notebooks-list--light.png index c6ae09e9748b5..ee6f207261e02 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--notebooks-list--light.png and b/frontend/__snapshots__/scenes-app-notebooks--notebooks-list--light.png differ diff --git a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--dark.png b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--dark.png new file mode 100644 index 0000000000000..c17dad0b17e61 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--light.png b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--light.png new file mode 100644 index 0000000000000..474c85c22515e Binary files /dev/null and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-no-email--light.png differ diff --git a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-with-email--dark.png b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-with-email--dark.png new file mode 100644 index 0000000000000..4f968d7f68b6f Binary files /dev/null and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-with-email--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-with-email--light.png b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-with-email--light.png new file mode 100644 index 0000000000000..d92a0899a7bd2 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-with-email--light.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section--dark.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section--dark.png index cfb811a0a8e4d..db9932e1f2320 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section--dark.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section--light.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section--light.png index 9973abab32ae8..bbbe76e720788 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section--light.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-targeting-section--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/__snapshots__/scenes-other-settings--settings-project--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png index aafe7723ef5bc..cb28d08f5b090 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project--light.png b/frontend/__snapshots__/scenes-other-settings--settings-project--light.png index 0b458d548f41e..31dbd5b34cfcf 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-project--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-3000/components/AlgoliaSearch.tsx b/frontend/src/layout/navigation-3000/components/AlgoliaSearch.tsx new file mode 100644 index 0000000000000..b6c52e3018133 --- /dev/null +++ b/frontend/src/layout/navigation-3000/components/AlgoliaSearch.tsx @@ -0,0 +1,268 @@ +import { IconCheckCircle } from '@posthog/icons' +import { LemonButton, LemonInput, LemonTag } from '@posthog/lemon-ui' +import algoliasearch from 'algoliasearch/lite' +import { useActions } from 'kea' +import { useEffect, useRef, useState } from 'react' +import { InstantSearch, useHits, useRefinementList, useSearchBox } from 'react-instantsearch' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import { List } from 'react-virtualized/dist/es/List' + +import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' +import { SidePanelTab } from '~/types' + +const searchClient = algoliasearch('7VNQB5W0TX', '37f41fd37095bc85af76ed4edc85eb5a') + +const rowRenderer = ({ key, index, style, hits, activeOption }: any): JSX.Element => { + const { slug, title, type, resolved } = hits[index] + return ( + // eslint-disable-next-line react/forbid-dom-props +
  • + + + +

    {title}

    + {type === 'question' && resolved && ( + + )} +
    +

    /{slug}

    +
    +
    +
  • + ) +} + +const Hits = ({ activeOption }: { activeOption?: number }): JSX.Element => { + const { hits } = useHits() + return ( +
      + + {({ height, width }: { height: number; width: number }) => ( + rowRenderer({ ...options, hits, activeOption })} + /> + )} + +
    + ) +} + +const SearchInput = ({ + value, + setValue, +}: { + value: string + setValue: React.Dispatch> +}): JSX.Element => { + const { refine } = useSearchBox() + + const handleChange = (value: string): void => { + setValue(value) + refine(value) + } + + return +} + +type Tag = { + type: string + label: string +} + +const tags: Tag[] = [ + { + type: 'all', + label: 'All', + }, + { + type: 'docs', + label: 'Docs', + }, + { + type: 'question', + label: 'Questions', + }, + { + type: 'tutorial', + label: 'Tutorials', + }, +] + +type SearchTagProps = Tag & { + active?: boolean + onClick: (type: string) => void +} + +const SearchTag = ({ type, label, active, onClick }: SearchTagProps): JSX.Element => { + const { refine, items } = useRefinementList({ attribute: 'type' }) + const itemCount = type !== 'all' && items.find(({ value }) => value === type)?.count + + const handleClick = (e: React.MouseEvent): void => { + e.stopPropagation() + onClick(type) + } + + useEffect(() => { + refine(type) + }, []) + + return ( + + ) +} + +const Tags = ({ + activeTag, + setActiveTag, +}: { + activeTag: string + setActiveTag: React.Dispatch> +}): JSX.Element => { + const handleClick = (type: string): void => { + setActiveTag(type) + } + + return ( +
      + {tags.map((tag) => { + const { type } = tag + return ( +
    • + +
    • + ) + })} +
    + ) +} + +const Search = (): JSX.Element => { + const { openSidePanel } = useActions(sidePanelStateLogic) + const { hits } = useHits() + const { items, refine } = useRefinementList({ attribute: 'type' }) + + const ref = useRef(null) + const [searchValue, setSearchValue] = useState('') + const [activeOption, setActiveOption] = useState() + const [activeTag, setActiveTag] = useState('all') + const [searchOpen, setSearchOpen] = useState(false) + + const handleKeyDown = (e: React.KeyboardEvent): void => { + switch (e.key) { + case 'Enter': { + if (activeOption !== undefined) { + openSidePanel(SidePanelTab.Docs, `https://posthog.com/${hits[activeOption].slug}`) + } + break + } + + case 'Escape': { + setSearchOpen(false) + break + } + case 'ArrowDown': { + e.preventDefault() + setActiveOption((currOption) => { + if (currOption === undefined || currOption >= hits.length - 1) { + return 0 + } + return currOption + 1 + }) + break + } + case 'ArrowUp': { + e.preventDefault() + setActiveOption((currOption) => { + if (currOption !== undefined) { + return currOption <= 0 ? hits.length - 1 : currOption - 1 + } + }) + break + } + case 'Tab': + case 'ArrowRight': { + e.preventDefault() + const currTagIndex = tags.findIndex(({ type }) => type === activeTag) + setActiveTag(tags[currTagIndex >= tags.length - 1 ? 0 : currTagIndex + 1].type) + break + } + case 'ArrowLeft': { + e.preventDefault() + const currTagIndex = tags.findIndex(({ type }) => type === activeTag) + setActiveTag(tags[currTagIndex <= 0 ? tags.length - 1 : currTagIndex - 1].type) + } + } + } + + useEffect(() => { + setSearchOpen(!!searchValue) + setActiveOption(0) + }, [searchValue]) + + useEffect(() => { + setActiveOption(0) + if (activeTag === 'all') { + const filteredItems = items.filter(({ value }) => tags.some(({ type }) => type === value)) + filteredItems.forEach(({ value, isRefined }) => { + if (!isRefined) { + refine(value) + } + }) + } else { + items.forEach(({ value, isRefined }) => { + if (isRefined) { + refine(value) + } + }) + refine(activeTag) + } + }, [activeTag]) + + useEffect(() => { + const handleClick = (e: any): void => { + if (!ref?.current?.contains(e.target)) { + setSearchOpen(false) + } + } + + window.addEventListener('click', handleClick) + + return () => { + window.removeEventListener('click', handleClick) + } + }, []) + + return ( +
    + + {searchOpen && ( +
    + + +
    + )} +
    + ) +} + +export default function AlgoliaSearch(): JSX.Element { + return ( + + + + ) +} diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx index abad5ff308743..2cbd7574fa1fb 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx @@ -1,11 +1,13 @@ import { Meta, StoryFn } from '@storybook/react' import { useActions } from 'kea' import { router } from 'kea-router' +import { supportLogic } from 'lib/components/Support/supportLogic' import { useEffect } from 'react' import { App } from 'scenes/App' import { urls } from 'scenes/urls' -import { mswDecorator } from '~/mocks/browser' +import { mswDecorator, useStorybookMocks } from '~/mocks/browser' +import organizationCurrent from '~/mocks/fixtures/api/organizations/@current/@current.json' import { SidePanelTab } from '~/types' import { sidePanelStateLogic } from './sidePanelStateLogic' @@ -59,3 +61,36 @@ export const SidePanelActivation: StoryFn = () => { export const SidePanelNotebooks: StoryFn = () => { return } + +export const SidePanelSupportNoEmail: StoryFn = () => { + return +} + +export const SidePanelSupportWithEmail: StoryFn = () => { + const { openEmailForm } = useActions(supportLogic) + useStorybookMocks({ + get: { + // TODO: setting available featues should be a decorator to make this easy + '/api/users/@me': () => [ + 200, + { + email: 'test@posthog.com', + first_name: 'Test Hedgehog', + organization: { + ...organizationCurrent, + available_product_features: [ + { + key: 'email_support', + name: 'Email support', + }, + ], + }, + }, + ], + }, + }) + useEffect(() => { + openEmailForm() + }, []) + return +} diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx index 8b6b61e55faa2..bcad53bdc9dfb 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx @@ -37,7 +37,7 @@ export const SIDE_PANEL_TABS: Record< noModalSupport: true, }, [SidePanelTab.Support]: { - label: 'Support', + label: 'Help', Icon: IconSupport, Content: SidePanelSupport, }, diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx index 381cd59181267..022b8d18dfbbb 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/SidePanelSupport.tsx @@ -1,51 +1,305 @@ -import { LemonButton } from '@posthog/lemon-ui' +import { + IconBug, + IconChevronDown, + IconFeatures, + IconFlask, + IconHelmet, + IconMap, + IconMessage, + IconRewindPlay, + IconStack, + IconToggle, + IconTrends, +} from '@posthog/icons' +import { LemonButton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { SupportForm } from 'lib/components/Support/SupportForm' import { supportLogic } from 'lib/components/Support/supportLogic' +import React from 'react' +import { billingLogic } from 'scenes/billing/billingLogic' +import { urls } from 'scenes/urls' +import { userLogic } from 'scenes/userLogic' -import { SidePanelTab } from '~/types' +import { AvailableFeature, ProductKey, SidePanelTab } from '~/types' +import AlgoliaSearch from '../../components/AlgoliaSearch' import { SidePanelPaneHeader } from '../components/SidePanelPaneHeader' +import { SIDE_PANEL_TABS } from '../SidePanel' import { sidePanelStateLogic } from '../sidePanelStateLogic' +const PRODUCTS = [ + { + name: 'Product OS', + slug: 'product-os', + icon: , + }, + { + name: 'Product analytics', + slug: 'product-analytics', + icon: , + }, + { + name: 'Session replay', + slug: 'session-replay', + icon: , + }, + { + name: 'Feature flags', + slug: 'feature-flags', + icon: , + }, + { + name: 'A/B testing', + slug: 'ab-testing', + icon: , + }, + { + name: 'Surveys', + slug: 'surveys', + icon: , + }, +] + +const Section = ({ title, children }: { title: string; children: React.ReactNode }): React.ReactElement => { + return ( +
    +

    {title}

    + {children} +
    + ) +} + +const SupportFormBlock = ({ onCancel }: { onCancel: () => void }): JSX.Element => { + const { billing } = useValues(billingLogic) + const supportResponseTimes = { + [AvailableFeature.EMAIL_SUPPORT]: '2-3 days', + [AvailableFeature.PRIORITY_SUPPORT]: '4-6 hours', + } + + return ( +
    +
    +
    +
    + Avg support response times +
    +
    + Explore options +
    +
    + {billing?.products + ?.find((product) => product.type == ProductKey.PLATFORM_AND_SUPPORT) + ?.plans?.map((plan, i) => ( + +
    + {i == 1 ? 'Pay-per-use' : plan.name} + {plan.current_plan && ( + <> + {' '} + (your plan) + + )} +
    +
    + {plan.features.some((f) => f.key == AvailableFeature.PRIORITY_SUPPORT) + ? supportResponseTimes[AvailableFeature.PRIORITY_SUPPORT] + : plan.features.some((f) => f.key == AvailableFeature.EMAIL_SUPPORT) + ? supportResponseTimes[AvailableFeature.EMAIL_SUPPORT] + : 'Community support only'} +
    +
    + ))} +
    + + + Submit + + + Cancel + +
    + ) +} + export const SidePanelSupport = (): JSX.Element => { const { closeSidePanel } = useActions(sidePanelStateLogic) + const { hasAvailableFeature } = useValues(userLogic) + const { openEmailForm, closeEmailForm } = useActions(supportLogic) + const { isEmailFormOpen } = useValues(supportLogic) const theLogic = supportLogic({ onClose: () => closeSidePanel(SidePanelTab.Support) }) const { title } = useValues(theLogic) - const { closeSupportForm } = useActions(theLogic) return ( <> - +
    - +
    + +
    + +
    +
      + {PRODUCTS.map((product, index) => ( +
    • + +
      + {product.icon} + + {product.name} + +
      +
      + +
      + +
    • + ))} +
    +
    -
    +
    +

    + Questions about features, how to's, or use cases? There are thousands of discussions in our + community forums. +

    - Submit - - - Cancel + Ask a question -
    + + +
    +
      +
    • + } + targetBlank + > + Report a bug + +
    • +
    • + } + targetBlank + > + See what we're building + +
    • +
    • + } + targetBlank + > + Vote on our roadmap + +
    • +
    • + } + targetBlank + > + Request a feature + +
    • +
    +
    + + {hasAvailableFeature(AvailableFeature.EMAIL_SUPPORT) ? ( +
    + {isEmailFormOpen ? ( + closeEmailForm()} /> + ) : ( +

    + Can't find what you need in the docs?{' '} + openEmailForm()}>Email an engineer +

    + )} +
    + ) : ( +
    +

    + Due to our large userbase, we're unable to offer email support to organizations on the + free plan. But we still want to help! +

    + +
      +
    1. + Search our docs +

      + We're constantly updating our docs and tutorials to provide the latest + information about installing, using, and troubleshooting. +

      +
    2. +
    3. + Ask a community question +

      + Many common (and niche) questions have already been resolved by users just like + you. (Our own engineers also keep an eye on the questions as they have time!){' '} + + Search community questions or ask your own. + +

      +
    4. +
    5. + + Explore PostHog partners + +

      + Third-party providers can help with installation and debugging of data issues. +

      +
    6. +
    7. + Upgrade to a paid plan +

      + Our paid plans offer email support.{' '} + + Explore options. + +

      +
    8. +
    +
    + )}
    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/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.scss b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.scss deleted file mode 100644 index 6bb87aa7f7ec7..0000000000000 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.scss +++ /dev/null @@ -1,40 +0,0 @@ -.property-filters-property-value { - min-width: 150px; - min-height: 40px; - background-color: var(--bg-light); - border: 1px solid var(--border); - border-radius: var(--radius); - - .ant-select-selection-search, - .ant-select-selection-placeholder { - display: flex; - align-items: center; - padding: 0 4px !important; - } - - &.ant-select-single { - .ant-select-selector { - height: unset; - min-height: 38px !important; - background-color: inherit; - border: none !important; - } - } - - &.ant-select-multiple { - .ant-select-selector { - height: 100% !important; - padding: 5px 40px 5px 11px; - background-color: inherit; - border: none !important; - - .ant-select-selection-search { - padding-left: 0 !important; - } - - .ant-select-selection-placeholder { - padding-left: 6px !important; - } - } - } -} diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx index fae22ccf8a00e..673bd426629bf 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx @@ -1,16 +1,12 @@ -import './PropertyValue.scss' - -import { AutoComplete } from 'antd' -import clsx from 'clsx' import { useActions, useValues } from 'kea' import { DateFilter } from 'lib/components/DateFilter/DateFilter' import { DurationPicker } from 'lib/components/DurationPicker/DurationPicker' import { PropertyFilterDatePicker } from 'lib/components/PropertyFilters/components/PropertyFilterDatePicker' import { propertyFilterTypeToPropertyDefinitionType } from 'lib/components/PropertyFilters/utils' import { dayjs } from 'lib/dayjs' -import { LemonSelectMultiple } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { LemonInputSelect } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect' import { formatDate, isOperatorDate, isOperatorFlag, isOperatorMulti, toString } from 'lib/utils' -import { useEffect, useRef, useState } from 'react' +import { useEffect } from 'react' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' import { PropertyFilterType, PropertyOperator, PropertyType } from '~/types' @@ -20,43 +16,26 @@ export interface PropertyValueProps { type: PropertyFilterType endpoint?: string // Endpoint to fetch options from placeholder?: string - className?: string onSet: CallableFunction value?: string | number | Array | null operator: PropertyOperator autoFocus?: boolean - allowCustom?: boolean eventNames?: string[] addRelativeDateTimeOptions?: boolean } -function matchesLowerCase(needle?: string, haystack?: string): boolean { - if (typeof haystack !== 'string' || typeof needle !== 'string') { - return false - } - return haystack.toLowerCase().indexOf(needle.toLowerCase()) > -1 -} - export function PropertyValue({ propertyKey, type, endpoint = undefined, placeholder = undefined, - className, onSet, value, operator, autoFocus = false, - allowCustom = true, eventNames = [], addRelativeDateTimeOptions = false, }: PropertyValueProps): JSX.Element { - // what the human has typed into the box - const [input, setInput] = useState(Array.isArray(value) ? '' : toString(value) ?? '') - - const [shouldBlur, setShouldBlur] = useState(false) - const autoCompleteRef = useRef(null) - const { formatPropertyValueForDisplay, describeProperty, options } = useValues(propertyDefinitionsModel) const { loadPropertyValues } = useActions(propertyDefinitionsModel) @@ -67,20 +46,6 @@ export function PropertyValue({ const isDurationProperty = propertyKey && describeProperty(propertyKey, propertyDefinitionType) === PropertyType.Duration - // update the input field if passed a new `value` prop - useEffect(() => { - if (value == null) { - setInput('') - } else if (!Array.isArray(value) && toString(value) !== input) { - const valueObject = options[propertyKey]?.values?.find((v) => v.id === value) - if (valueObject) { - setInput(toString(valueObject.name)) - } else { - setInput(toString(value)) - } - } - }, [value]) - const load = (newInput: string | undefined): void => { loadPropertyValues({ endpoint, @@ -91,114 +56,26 @@ export function PropertyValue({ }) } - function setValue(newValue: PropertyValueProps['value']): void { - onSet(newValue) - if (isMultiSelect) { - setInput('') - } - } + const setValue = (newValue: PropertyValueProps['value']): void => onSet(newValue) useEffect(() => { load('') }, [propertyKey]) - useEffect(() => { - if (input === '' && shouldBlur) { - ;(document.activeElement as HTMLElement)?.blur() - setShouldBlur(false) - } - }, [input, shouldBlur]) - - const displayOptions = (options[propertyKey]?.values || []).filter( - (option) => input === '' || matchesLowerCase(input, toString(option?.name)) - ) + const displayOptions = options[propertyKey]?.values || [] - const commonInputProps = { - onSearch: (newInput: string) => { - setInput(newInput) - if (!Object.keys(options).includes(newInput) && !(operator && isOperatorFlag(operator))) { - load(newInput.trim()) - } - }, - ['data-attr']: 'prop-val', - dropdownMatchSelectWidth: 350, - placeholder, - allowClear: Boolean(value), - onKeyDown: (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - setInput('') - setShouldBlur(true) - return - } - if (!isMultiSelect && e.key === 'Enter') { - // We have not explicitly selected a dropdown item by pressing the up/down keys; or the ref is unavailable - if ( - !autoCompleteRef.current || - autoCompleteRef.current?.querySelectorAll?.('.ant-select-item-option-active')?.length === 0 - ) { - setValue(input) - } - } - }, - handleBlur: () => { - if (input != '') { - if (Array.isArray(value) && !value.includes(input)) { - setValue([...value, ...[input]]) - } else if (!Array.isArray(value)) { - setValue(input) - } - setInput('') - } - }, + const onSearchTextChange = (newInput: string): void => { + if (!Object.keys(options).includes(newInput) && !(operator && isOperatorFlag(operator))) { + load(newInput.trim()) + } } - if (isMultiSelect) { - const formattedValues = ( - value === null || value === undefined ? [] : Array.isArray(value) ? value : [value] - ).map((label) => String(formatPropertyValueForDisplay(propertyKey, label))) - return ( - { - setValue(nextVal) - }} - onBlur={commonInputProps.handleBlur} - // TODO: When LemonSelectMultiple is free of AntD, add footnote that pressing comma applies the value - options={Object.fromEntries([ - ...displayOptions.map(({ name: _name }, index) => { - const name = toString(_name) - return [ - name, - { - label: name, - labelComponent: ( - - {name === '' ? ( - (empty string) - ) : ( - formatPropertyValueForDisplay(propertyKey, name) - )} - - ), - }, - ] - }), - ])} - /> - ) + if (isDurationProperty) { + return } - if (isDateTimeProperty && addRelativeDateTimeOptions) { - if (operator === PropertyOperator.IsDateExact) { + if (isDateTimeProperty) { + if (!addRelativeDateTimeOptions || operator === PropertyOperator.IsDateExact) { return ( ) @@ -241,52 +118,32 @@ export function PropertyValue({ ) } - return isDateTimeProperty ? ( - - ) : isDurationProperty ? ( - - ) : ( - { - setInput('') - setValue('') - }} - onChange={(val) => { - setInput(toString(val)) - }} - onSelect={(val, option) => { - setInput(option.title) - setValue(toString(val).trim()) - }} - ref={autoCompleteRef} - > - {[ - ...(input && allowCustom && !displayOptions.some(({ name }) => input === toString(name)) - ? [ - - Specify: {input} - , - ] - : []), - ...displayOptions.map(({ name: _name, id }, index) => { - const name = toString(_name) - return ( - - {name} - - ) - }), - ]} - + const formattedValues = (value === null || value === undefined ? [] : Array.isArray(value) ? value : [value]).map( + (label) => String(formatPropertyValueForDisplay(propertyKey, label)) + ) + + return ( + (isMultiSelect ? setValue(nextVal) : setValue(nextVal[0]))} + onInputChange={onSearchTextChange} + placeholder={placeholder} + options={displayOptions.map(({ name: _name }, index) => { + const name = toString(_name) + return { + key: name, + label: name, + labelComponent: ( + + {name === '' ? (empty string) : formatPropertyValueForDisplay(propertyKey, name)} + + ), + } + })} + /> ) } 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/lib/components/Subscriptions/subscriptionLogic.ts b/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts index cf673b42dbe4c..ab5108074621b 100644 --- a/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts +++ b/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts @@ -78,7 +78,7 @@ export const subscriptionLogic = kea([ : undefined, memberOfSlackChannel: target_type == 'slack' - ? !values.isMemberOfSlackChannel(target_value) + ? target_value && !values.isMemberOfSlackChannel(target_value) ? 'Please add the PostHog Slack App to the selected channel' : undefined : undefined, diff --git a/frontend/src/lib/components/Subscriptions/utils.tsx b/frontend/src/lib/components/Subscriptions/utils.tsx index 136548cfdfcf1..370c6855d6d1f 100644 --- a/frontend/src/lib/components/Subscriptions/utils.tsx +++ b/frontend/src/lib/components/Subscriptions/utils.tsx @@ -1,7 +1,7 @@ import { IconLetter } from '@posthog/icons' import { LemonSelectOptions } from '@posthog/lemon-ui' import { IconSlack, IconSlackExternal } from 'lib/lemon-ui/icons' -import { LemonSelectMultipleOptionItem } from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' +import { LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect' import { range } from 'lib/utils' import { urls } from 'scenes/urls' @@ -84,7 +84,7 @@ export const timeOptions: LemonSelectOptions = range(0, 24).map((x) => ( export const getSlackChannelOptions = ( value: string, slackChannels?: SlackChannelType[] | null -): LemonSelectMultipleOptionItem[] => { +): LemonInputSelectOption[] => { return slackChannels ? slackChannels.map((x) => ({ key: `${x.id}|#${x.name}`, diff --git a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx index ed34a261c6103..d6fc5af539bcb 100644 --- a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx +++ b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx @@ -8,13 +8,10 @@ import { IconChevronLeft } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonField } from 'lib/lemon-ui/LemonField' +import { LemonInputSelect, LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { LemonSelect } from 'lib/lemon-ui/LemonSelect' -import { - LemonSelectMultiple, - LemonSelectMultipleOptionItem, -} from 'lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { useEffect, useMemo } from 'react' import { membersLogic } from 'scenes/organization/membersLogic' @@ -86,7 +83,7 @@ export function EditSubscription({ }, [subscription?.target_type, slackIntegration]) // If slackChannels aren't loaded, make sure we display only the channel name and not the actual underlying value - const slackChannelOptions: LemonSelectMultipleOptionItem[] = useMemo( + const slackChannelOptions: LemonInputSelectOption[] = useMemo( () => getSlackChannelOptions(subscription?.target_value, slackChannels), [slackChannels, subscription?.target_value] ) @@ -199,11 +196,12 @@ export function EditSubscription({ help="Enter the email addresses of the users you want to share with" > {({ value, onChange }) => ( - onChange(val.join(','))} + onChange(val.join(','))} value={value?.split(',').filter(Boolean)} disabled={emailDisabled} - mode="multiple-custom" + mode="multiple" + allowCustomValues data-attr="subscribed-emails" options={usersLemonSelectOptions(meFirstMembers.map((x) => x.user))} loading={membersLoading} @@ -276,12 +274,13 @@ export function EditSubscription({ } > {({ value, onChange }) => ( - onChange(val)} - value={value} + onChange(val[0] ?? null)} + value={value ? [value] : []} disabled={slackDisabled} mode="single" data-attr="select-slack-channel" + placeholder="Select a channel..." options={slackChannelOptions} loading={slackChannelsLoading} /> diff --git a/frontend/src/lib/components/Support/SupportForm.tsx b/frontend/src/lib/components/Support/SupportForm.tsx index 7f92d89f1445a..b23fdd83a3501 100644 --- a/frontend/src/lib/components/Support/SupportForm.tsx +++ b/frontend/src/lib/components/Support/SupportForm.tsx @@ -1,4 +1,4 @@ -import { IconBug, IconQuestion } from '@posthog/icons' +import { IconBug, IconInfo, IconQuestion } from '@posthog/icons' import { LemonBanner, LemonInput, @@ -6,6 +6,7 @@ import { LemonSegmentedButtonOption, lemonToast, Link, + Tooltip, } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' @@ -90,9 +91,12 @@ export function SupportForm(): JSX.Element | null { )} - + + + + {posthog.getFeatureFlag('show-troubleshooting-docs-in-support-form') === 'test-replay-banner' && sendSupportRequest.target_area === 'session_replay' && ( @@ -127,18 +131,6 @@ export function SupportForm(): JSX.Element | null { )} - - - - - ({ - label: value, - value: key, - }))} - /> - )} + + <> +
    + + + Definitions + +
    + ({ + label: value, + value: key, + }))} + /> + +
    ) } diff --git a/frontend/src/lib/components/Support/supportLogic.ts b/frontend/src/lib/components/Support/supportLogic.ts index 003ad6a4a9e6e..9c4cbd02712b5 100644 --- a/frontend/src/lib/components/Support/supportLogic.ts +++ b/frontend/src/lib/components/Support/supportLogic.ts @@ -45,7 +45,7 @@ function getSentryLink(user: UserType | null, cloudRegion: Region | null | undef } const SUPPORT_TICKET_KIND_TO_TITLE: Record = { - support: 'Ask a question', + support: 'Contact support', feedback: 'Give feedback', bug: 'Report a bug', } @@ -237,6 +237,8 @@ export const supportLogic = kea([ openSupportForm: (values: Partial) => values, submitZendeskTicket: (form: SupportFormFields) => form, updateUrlParams: true, + openEmailForm: true, + closeEmailForm: true, })), reducers(() => ({ isSupportFormOpen: [ @@ -246,6 +248,13 @@ export const supportLogic = kea([ closeSupportForm: () => false, }, ], + isEmailFormOpen: [ + false, + { + openEmailForm: () => true, + closeEmailForm: () => false, + }, + ], })), forms(({ actions, values }) => ({ sendSupportRequest: { 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/components/UserSelectItem.tsx b/frontend/src/lib/components/UserSelectItem.tsx index 7990f4c8a7301..903440db365e4 100644 --- a/frontend/src/lib/components/UserSelectItem.tsx +++ b/frontend/src/lib/components/UserSelectItem.tsx @@ -1,4 +1,4 @@ -import { LemonSelectMultipleOptionItem } from 'lib/lemon-ui/LemonSelectMultiple' +import { LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { UserBasicType, UserType } from '~/types' @@ -21,7 +21,7 @@ export function UserSelectItem({ user }: UserSelectItemProps): JSX.Element { export function usersLemonSelectOptions( users: (UserBasicType | UserType)[], key: 'email' | 'uuid' = 'email' -): LemonSelectMultipleOptionItem[] { +): LemonInputSelectOption[] { return users.map((user) => ({ key: user[key], label: `${user.first_name} ${user.email}`, 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/LemonInput/LemonInput.scss b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss index 28a84357dadeb..934cdaeb36254 100644 --- a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss +++ b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss @@ -1,9 +1,11 @@ .LemonInput { + --lemon-input-height: calc(2.125rem + 3px); // Medium size button height + button shadow height; + display: flex; gap: 0.25rem; align-items: center; - justify-content: center; - height: calc(2.125rem + 3px); // Medium size button height + button shadow height + justify-content: left; + height: var(--lemon-input-height); padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.25rem; @@ -33,6 +35,7 @@ } .LemonInput__input { + flex: 1; align-self: stretch; // Improves selectability width: 100%; text-overflow: ellipsis; @@ -56,7 +59,8 @@ } &.LemonInput--small { - height: 2rem; + --lemon-input-height: 2rem; + padding: 0.125rem 0.25rem; .LemonIcon { @@ -85,4 +89,10 @@ width: 100%; max-width: 100%; } + + .LemonInputSelect & { + flex-wrap: wrap; + height: auto; + min-height: var(--lemon-input-height); + } } diff --git a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx new file mode 100644 index 0000000000000..796d1794b4c89 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx @@ -0,0 +1,102 @@ +import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { capitalizeFirstLetter } from 'lib/utils' +import { useState } from 'react' + +import { ProfilePicture } from '../ProfilePicture' +import { LemonInputSelect, LemonInputSelectProps } from './LemonInputSelect' + +const names = ['ben', 'marius', 'paul', 'tiina', 'tim', 'james', 'neil', 'tom', 'annika', 'thomas'] + +type Story = StoryObj +const meta: Meta = { + title: 'Lemon UI/Lemon Input Select', + component: LemonInputSelect, + args: { + options: names.map((x, i) => ({ + key: `user-${i}`, + labelComponent: ( + + + + {capitalizeFirstLetter(x)} {`<${x}@posthog.com>`} + + + ), + label: `${x} ${x}@posthog.com>`, + })), + }, + tags: ['autodocs'], +} +export default meta + +const Template: StoryFn = (props: LemonInputSelectProps) => { + const [value, setValue] = useState(props.value || []) + return +} + +export const Default: Story = Template.bind({}) +Default.args = { + placeholder: 'Pick one email', + mode: 'single', +} + +export const MultipleSelect: Story = Template.bind({}) +MultipleSelect.args = { + placeholder: 'Enter emails...', + mode: 'multiple', +} + +export const MultipleSelectWithCustom: Story = Template.bind({}) +MultipleSelectWithCustom.args = { + placeholder: 'Enter any email...', + mode: 'multiple', + allowCustomValues: true, +} + +export const Disabled: Story = Template.bind({}) +Disabled.args = { + mode: 'single', + placeholder: 'Disabled...', + disabled: true, +} + +export const Loading: Story = Template.bind({}) +Loading.args = { + mode: 'single', + placeholder: 'Loading...', + options: [], + loading: true, +} +Loading.parameters = { + testOptions: { + waitForLoadersToDisappear: false, + }, +} + +export const NoOptions: Story = Template.bind({}) +NoOptions.args = { + mode: 'multiple', + allowCustomValues: true, + placeholder: 'No options...', + options: [], +} + +export const SingleOptionWithCustom: Story = Template.bind({}) +SingleOptionWithCustom.args = { + mode: 'single', + allowCustomValues: true, + placeholder: 'Only one option allowed but can be custom', +} + +export const PrefilledManyValues: Story = Template.bind({}) +PrefilledManyValues.args = { + mode: 'multiple', + allowCustomValues: true, + value: names.map((_, i) => `user-${i}`), +} diff --git a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx new file mode 100644 index 0000000000000..967f18e323753 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx @@ -0,0 +1,269 @@ +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' +import { range } from 'lib/utils' +import { useEffect, useMemo, useRef, useState } from 'react' + +import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut' + +import { LemonButton } from '../LemonButton' +import { LemonDropdown } from '../LemonDropdown' +import { LemonInput } from '../LemonInput' +import { PopoverReferenceContext } from '../Popover' + +export interface LemonInputSelectOption { + key: string + label: string + labelComponent?: React.ReactNode +} + +export type LemonInputSelectProps = { + options?: LemonInputSelectOption[] + value?: string[] | null + disabled?: boolean + loading?: boolean + placeholder?: string + disableFiltering?: boolean + mode: 'multiple' | 'single' + allowCustomValues?: boolean + onChange?: (newValue: string[]) => void + onInputChange?: (newValue: string) => void + 'data-attr'?: string +} + +export function LemonInputSelect({ + placeholder, + options = [], + value, + loading, + onChange, + onInputChange, + mode, + disabled, + disableFiltering = false, + allowCustomValues = false, + ...props +}: LemonInputSelectProps): JSX.Element { + const [showPopover, setShowPopover] = useState(false) + const [inputValue, _setInputValue] = useState('') + const popoverFocusRef = useRef(false) + const inputRef = useRef(null) + const [selectedIndex, setSelectedIndex] = useState(0) + const values = value ?? [] + + const visibleOptions = useMemo(() => { + const res: LemonInputSelectOption[] = [] + const customValues = [...values] + + options.forEach((option) => { + // Remove from the custom values list if it's in the options + + if (customValues.includes(option.key)) { + customValues.splice(customValues.indexOf(option.key), 1) + } + + // Check for filtering + if (inputValue && !disableFiltering && !option.label.toLowerCase().includes(inputValue.toLowerCase())) { + return + } + + res.push(option) + }) + + // Custom values are always shown before the list + if (customValues.length) { + customValues.forEach((value) => { + res.unshift({ key: value, label: value }) + }) + } + + // Finally we show the input value if custom values are allowed and it's not in the list + if (allowCustomValues && inputValue && !values.includes(inputValue)) { + res.unshift({ key: inputValue, label: inputValue }) + } + + return res + }, [options, inputValue, value]) + + // Reset the selected index when the visible options change + useEffect(() => { + setSelectedIndex(0) + }, [visibleOptions.length]) + + const setInputValue = (newValue: string): void => { + _setInputValue(newValue) + onInputChange?.(inputValue) + } + + const _onActionItem = (item: string): void => { + let newValues = [...values] + if (values.includes(item)) { + // Remove the item + if (mode === 'single') { + newValues = [] + } else { + newValues.splice(values.indexOf(item), 1) + } + } else { + // Add the item + if (mode === 'single') { + newValues = [item] + } else { + newValues.push(item) + } + + setInputValue('') + } + + onChange?.(newValues) + } + + const _onBlur = (): void => { + // We need to add a delay as a click could be in the popover or the input wrapper which refocuses + setTimeout(() => { + if (popoverFocusRef.current) { + popoverFocusRef.current = false + inputRef.current?.focus() + _onFocus() + return + } + if (allowCustomValues && inputValue.trim() && !values.includes(inputValue)) { + _onActionItem(inputValue.trim()) + } else { + setInputValue('') + } + setShowPopover(false) + }, 100) + } + + const _onFocus = (): void => { + setShowPopover(true) + popoverFocusRef.current = true + } + + const _onKeyDown = (e: React.KeyboardEvent): void => { + if (e.key === 'Enter') { + e.preventDefault() + + const itemToAdd = visibleOptions[selectedIndex]?.key + if (itemToAdd) { + _onActionItem(visibleOptions[selectedIndex]?.key) + } + } else if (e.key === 'Backspace') { + if (!inputValue) { + e.preventDefault() + const newValues = [...values] + newValues.pop() + onChange?.(newValues) + } + } else if (e.key === 'ArrowDown') { + e.preventDefault() + setSelectedIndex(Math.min(selectedIndex + 1, visibleOptions.length - 1)) + } else if (e.key === 'ArrowUp') { + e.preventDefault() + setSelectedIndex(Math.max(selectedIndex - 1, 0)) + } + } + + // TRICKY: We don't want the popover to affect the snack buttons + const prefix = ( + + <> + {values.map((value) => { + const option = options.find((option) => option.key === value) ?? { + label: value, + labelComponent: null, + } + return ( + <> + _onActionItem(value)}> + {option?.labelComponent ?? option?.label} + + + ) + })} + + + ) + + return ( + { + popoverFocusRef.current = false + setShowPopover(false) + }} + onClickInside={(e) => { + popoverFocusRef.current = true + e.stopPropagation() + }} + overlay={ +
    + {visibleOptions.length ? ( + visibleOptions?.map((option, index) => { + const isHighlighted = index === selectedIndex + return ( + _onActionItem(option.key)} + onMouseEnter={() => setSelectedIndex(index)} + > + + {option.labelComponent ?? option.label} + {isHighlighted ? ( + + {' '} + {!values.includes(option.key) + ? mode === 'single' + ? 'select' + : 'add' + : mode === 'single' + ? 'unselect' + : 'remove'} + + ) : undefined} + + + ) + }) + ) : loading ? ( + <> + {range(5).map((x) => ( +
    + + +
    + ))} + + ) : ( +

    + {allowCustomValues + ? 'Start typing and press Enter to add options' + : `No options matching "${inputValue}"`} +

    + )} +
    + } + > + + + +
    + ) +} diff --git a/frontend/src/lib/lemon-ui/LemonInputSelect/index.ts b/frontend/src/lib/lemon-ui/LemonInputSelect/index.ts new file mode 100644 index 0000000000000..4e01ced8508e3 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonInputSelect/index.ts @@ -0,0 +1 @@ +export * from './LemonInputSelect' 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/LemonSelectMultiple/LemonSelectMultiple.scss b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss deleted file mode 100644 index 67200ac17bdf3..0000000000000 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.scss +++ /dev/null @@ -1,118 +0,0 @@ -.LemonSelectMultiple { - .ant-select { - width: 100%; - - .ant-select-selector, - &.ant-select-single .ant-select-selector { - min-height: 2.125rem; - padding: 0.25rem; - font-size: 0.875rem; - line-height: 1.25rem; - text-align: left; - background: var(--bg-light); - border: 1px solid var(--border); - border-radius: var(--radius); - - .ant-select-selection-overflow { - gap: 0.25rem; - } - } - - &:not(.ant-select-disabled):hover, - &.ant-select-focused:not(.ant-select-disabled), - &:not(.ant-select-disabled):active { - .ant-select-selector { - background: var(--bg-light); - border-color: var(--border-bold); - box-shadow: none; - } - } - - &:not(.ant-select-disabled):active { - .ant-select-selector { - color: var(--primary-active); - } - } - - .ant-select-selection-placeholder { - color: var(--muted); - } - - &.ant-select-single { - .ant-select-selector { - box-sizing: border-box; - height: 40px; - - .ant-select-selection-search-input { - height: 38px; - } - - .ant-select-selection-placeholder { - padding-left: 0.4rem; - } - - .ant-select-selection-item { - padding-left: 0.4rem; - } - } - } - - .ant-select-arrow { - display: none; - } - } -} - -.LemonSelectMultipleDropdown { - padding: 0.5rem; - margin: -4px 0; // Counteract antd wrapper - background: var(--bg-light); - border: 1px solid var(--primary-3000); - border-radius: var(--radius); - - .ant-select-item { - padding: 0; - padding-bottom: 0.2rem; - background: none; - - .ant-select-item-option-content { - height: 40px; - padding: 0.25rem 0.5rem; - cursor: pointer; - border-radius: var(--radius); - } - - &.ant-select-item-option-active { - .ant-select-item-option-content { - background: var(--primary-bg-hover); - } - } - - &.ant-select-item-option-selected { - .ant-select-item-option-content { - background: var(--primary-bg-active); - } - } - - .ant-select-item-option-state { - display: none; - } - } - - .ant-select-item-empty { - padding: 0; - } - - .ant-select-item-option-content { - display: flex; - align-items: center; - } - - .LemonSelectMultipleDropdown__skeleton { - display: flex; - gap: 0.5rem; - align-items: center; - height: 40px; - padding: 0 0.25rem; - } -} diff --git a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx deleted file mode 100644 index baa2f805f48e4..0000000000000 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.stories.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { capitalizeFirstLetter } from 'lib/utils' -import { useState } from 'react' - -import { ProfilePicture } from '../ProfilePicture' -import { LemonSelectMultiple, LemonSelectMultipleProps } from './LemonSelectMultiple' - -type Story = StoryObj -const meta: Meta = { - title: 'Lemon UI/Lemon SelectMultiple', - component: LemonSelectMultiple, - args: { - options: ['ben', 'marius', 'paul', 'tiina', 'li'].reduce( - (acc, x, i) => ({ - ...acc, - [`user-${i}`]: { - labelComponent: ( - - - - {capitalizeFirstLetter(x)} {`<${x}@posthog.com>`} - - - ), - label: `${x} ${x}@posthog.com>`, - }, - }), - {} - ), - }, - tags: ['autodocs'], -} -export default meta - -const Template: StoryFn = (props: LemonSelectMultipleProps) => { - const [value, setValue] = useState(props.value || []) - return -} - -export const Default: Story = Template.bind({}) -Default.args = { - placeholder: 'Pick one email', -} - -export const MultipleSelect: Story = Template.bind({}) -MultipleSelect.args = { - placeholder: 'Enter emails...', - mode: 'multiple', -} - -export const MultipleSelectWithCustom: Story = Template.bind({}) -MultipleSelectWithCustom.args = { - placeholder: 'Enter any email...', - mode: 'multiple-custom', -} - -export const Disabled: Story = Template.bind({}) -Disabled.args = { - placeholder: 'Disabled...', - disabled: true, -} - -export const Loading: Story = Template.bind({}) -Loading.args = { - placeholder: 'Loading...', - options: [], - loading: true, -} -Loading.parameters = { - testOptions: { - waitForLoadersToDisappear: false, - }, -} - -export const NoOptions: Story = Template.bind({}) -NoOptions.args = { - mode: 'multiple-custom', - placeholder: 'No options...', - options: [], -} diff --git a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx deleted file mode 100644 index 9c085799bb855..0000000000000 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import './LemonSelectMultiple.scss' - -import { Select } from 'antd' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' -import { range } from 'lib/utils' -import { ReactNode } from 'react' - -export interface LemonSelectMultipleOption { - label: string - disabled?: boolean - 'data-attr'?: string - labelComponent?: React.ReactNode -} - -export interface LemonSelectMultipleOptionItem extends LemonSelectMultipleOption { - key: string -} - -export type LemonSelectMultipleOptions = Record - -export type LemonSelectMultipleProps = { - selectClassName?: string - options?: LemonSelectMultipleOptions | LemonSelectMultipleOptionItem[] - value?: string | string[] | null - disabled?: boolean - loading?: boolean - placeholder?: string - labelInValue?: boolean - onSearch?: (value: string) => void - onFocus?: () => void - onBlur?: () => void - filterOption?: boolean - mode?: 'single' | 'multiple' | 'multiple-custom' - onChange?: ((newValue: string) => void) | ((newValue: string[]) => void) - 'data-attr'?: string -} - -export type LabelInValue = { value: string; label: ReactNode } - -export function LemonSelectMultiple({ - value, - options, - disabled, - loading, - placeholder, - labelInValue, - onChange, - onSearch, - onFocus, - onBlur, - filterOption = true, - mode = 'single', - selectClassName, - ...props -}: LemonSelectMultipleProps): JSX.Element { - const optionsAsList: LemonSelectMultipleOptionItem[] = Array.isArray(options) - ? options - : Object.entries(options || {}).map(([key, option]) => ({ - key: key, - ...option, - })) - - const antOptions = optionsAsList.map((option) => ({ - key: option.key, - value: option.key, - label: option.labelComponent || option.label, - labelString: option.label, - })) - - return ( -
    -