diff --git a/.github/workflows/replay-capture.yml b/.github/workflows/replay-capture.yml new file mode 100644 index 0000000000000..20f5df785763e --- /dev/null +++ b/.github/workflows/replay-capture.yml @@ -0,0 +1,35 @@ +name: Vector Replay Capture Tests + +on: + workflow_dispatch: + pull_request: + paths: + - vector/** + - .github/workflows/replay-capture.yml + + workflow_call: + +jobs: + vector-test: + name: Vector test + runs-on: ubuntu-20.04 + env: + QUOTA_LIMITED_TEAMS_PATH: vector/replay-capture/tests/quota_limited_teams.csv + OVERFLOW_SESSIONS_PATH: vector/replay-capture/tests/overflow_sessions.csv + KAFKA_BOOSTRAP_SERVERS: dummy:9092 + KAFKA_EVENTS_TOPIC: session_recording_snapshot_item_events + KAFKA_OVERFLOW_TOPIC: session_recording_snapshot_item_overflow + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Vector + run: | + wget https://github.com/vectordotdev/vector/releases/download/v0.40.0/vector-0.40.0-x86_64-unknown-linux-gnu.tar.gz + tar xzvf vector-0.40.0-x86_64-unknown-linux-gnu.tar.gz ./vector-x86_64-unknown-linux-gnu/bin/vector + sudo mv ./vector-x86_64-unknown-linux-gnu/bin/vector /usr/bin/vector + + - name: Run vector tests + run: | + yq -i e 'explode(.)' vector/replay-capture/vector.yaml + vector test vector/replay-capture/*.yaml diff --git a/.github/workflows/vector-docker-build-deploy.yml b/.github/workflows/vector-docker-build-deploy.yml new file mode 100644 index 0000000000000..44b86c24cd22a --- /dev/null +++ b/.github/workflows/vector-docker-build-deploy.yml @@ -0,0 +1,104 @@ +name: Build and deploy replay capture container images + +on: + workflow_dispatch: + push: + paths: + - 'vector/**' + - '.github/workflows/vector-docker-build-deploy.yml' + branches: + - 'master' + +jobs: + build: + name: Build and publish container image + runs-on: depot-ubuntu-22.04-4 + permissions: + id-token: write # allow issuing OIDC tokens for this workflow run + contents: read # allow reading the repo contents + packages: write # allow push to ghcr.io + + outputs: + digest: ${{ steps.docker_build.outputs.digest }} + + defaults: + run: + working-directory: vector/ + + steps: + - name: Check Out Repo + # Checkout project code + # Use sparse checkout to only select files in vector directory + # Turning off cone mode ensures that files in the project root are not included during checkout + uses: actions/checkout@v4 + with: + sparse-checkout: 'vector/' + sparse-checkout-cone-mode: false + + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + logout: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/posthog/posthog/replay-capture + tags: | + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push image + id: docker_build + uses: docker/build-push-action@v5 + with: + context: ./vector/replay-capture/ + file: ./vector/replay-capture/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/arm64 + + deploy: + runs-on: ubuntu-latest + needs: build + steps: + - name: get deployer token + id: deployer + uses: getsentry/action-github-app-token@v3 + with: + app_id: ${{ secrets.DEPLOYER_APP_ID }} + private_key: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} + + - name: Trigger livestream deployment + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ steps.deployer.outputs.token }} + repository: PostHog/charts + event-type: commit_state_update + client-payload: | + { + "values": { + "image": { + "sha": "${{ needs.build.outputs.digest }}" + } + }, + "release": "replay-capture-vector", + "commit": ${{ toJson(github.event.head_commit) }}, + "repository": ${{ toJson(github.repository) }}, + "labels": [] + } diff --git a/cypress/e2e/dashboard.cy.ts b/cypress/e2e/dashboard.cy.ts index c67a21f2e24e8..3af5333100eef 100644 --- a/cypress/e2e/dashboard.cy.ts +++ b/cypress/e2e/dashboard.cy.ts @@ -14,7 +14,7 @@ describe('Dashboard', () => { it('Dashboards loaded', () => { cy.get('h1').should('contain', 'Dashboards') // Breadcrumbs work - cy.get('[data-attr=breadcrumb-organization]').should('contain', 'Hogflix') + cy.get('[data-attr=breadcrumb-organization]').should('contain', 'H') // "H" as the lettermark of "Hogflix" cy.get('[data-attr=breadcrumb-project]').should('contain', 'Hogflix Demo App') cy.get('[data-attr=breadcrumb-Dashboards]').should('have.text', 'Dashboards') }) @@ -233,7 +233,7 @@ describe('Dashboard', () => { cy.get('.InsightCard').its('length').should('be.gte', 2) // Breadcrumbs work - cy.get('[data-attr=breadcrumb-organization]').should('contain', 'Hogflix') + cy.get('[data-attr=breadcrumb-organization]').should('contain', 'H') // "H" as the lettermark of "Hogflix" cy.get('[data-attr=breadcrumb-project]').should('contain', 'Hogflix Demo App') cy.get('[data-attr=breadcrumb-Dashboards]').should('have.text', 'Dashboards') cy.get('[data-attr^="breadcrumb-Dashboard:"]').should('have.text', TEST_DASHBOARD_NAME + 'UnnamedCancelSave') diff --git a/cypress/e2e/insights.cy.ts b/cypress/e2e/insights.cy.ts index aeed4a2e8a868..19a453afbe620 100644 --- a/cypress/e2e/insights.cy.ts +++ b/cypress/e2e/insights.cy.ts @@ -13,7 +13,7 @@ describe('Insights', () => { it('Saving an insight sets breadcrumbs', () => { createInsight('insight name') - cy.get('[data-attr=breadcrumb-organization]').should('contain', 'Hogflix') + cy.get('[data-attr=breadcrumb-organization]').should('contain', 'H') // "H" as the lettermark of "Hogflix" cy.get('[data-attr=breadcrumb-project]').should('contain', 'Hogflix Demo App') cy.get('[data-attr=breadcrumb-SavedInsights]').should('have.text', 'Product analytics') cy.get('[data-attr^="breadcrumb-Insight:"]').should('have.text', 'insight name') diff --git a/docker-compose.base.yml b/docker-compose.base.yml index 4303d86a91350..0f75bc58ec078 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -105,7 +105,7 @@ services: restart: on-failure capture: - image: ghcr.io/posthog/capture:main + image: ghcr.io/posthog/posthog/capture:master restart: on-failure environment: ADDRESS: '0.0.0.0:3000' @@ -113,6 +113,26 @@ services: KAFKA_HOSTS: 'kafka:9092' REDIS_URL: 'redis://redis:6379/' + replay-capture: + image: ghcr.io/posthog/posthog/replay-capture:master + build: + context: vector/replay-capture + restart: on-failure + entrypoint: ['sh', '-c'] + command: + - | + set -x + # seed empty required data files + mkdir -p /etc/vector/data + echo "token" > /etc/vector/data/quota_limited_teams.csv + echo "session_id" > /etc/vector/data/overflow_sessions.csv + exec vector -v --watch-config + environment: + KAFKA_EVENTS_TOPIC: session_recording_snapshot_item_events + KAFKA_OVERFLOW_TOPIC: session_recording_snapshot_item_overflow + KAFKA_BOOSTRAP_SERVERS: 'kafka:9092' + REDIS_URL: 'redis://redis:6379/' + plugins: command: ./bin/plugin-server --no-restart-loop restart: on-failure diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f79697c73fbfd..510570ce2ee90 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -117,6 +117,17 @@ services: - redis - kafka + # Optional capture + replay-capture: + extends: + file: docker-compose.base.yml + service: replay-capture + ports: + - 3001:8000 + depends_on: + - redis + - kafka + livestream: extends: file: docker-compose.base.yml diff --git a/ee/api/test/__snapshots__/test_organization_resource_access.ambr b/ee/api/test/__snapshots__/test_organization_resource_access.ambr index 5c57c02e79444..49b6f62b22d8c 100644 --- a/ee/api/test/__snapshots__/test_organization_resource_access.ambr +++ b/ee/api/test/__snapshots__/test_organization_resource_access.ambr @@ -36,6 +36,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -119,6 +120,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -220,6 +222,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -245,6 +248,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/ee/clickhouse/queries/experiments/funnel_experiment_result.py b/ee/clickhouse/queries/experiments/funnel_experiment_result.py index 1f353829fe29f..ea95e177a5c80 100644 --- a/ee/clickhouse/queries/experiments/funnel_experiment_result.py +++ b/ee/clickhouse/queries/experiments/funnel_experiment_result.py @@ -6,6 +6,8 @@ from numpy.random import default_rng from rest_framework.exceptions import ValidationError +import scipy.stats as stats +from sentry_sdk import capture_exception from ee.clickhouse.queries.experiments import ( CONTROL_VARIANT_KEY, @@ -111,6 +113,8 @@ def get_results(self, validate: bool = True): } significance_code, loss = self.are_results_significant(control_variant, test_variants, probabilities) + + credible_intervals = calculate_credible_intervals([control_variant, *test_variants]) except ValidationError: if validate: raise @@ -124,6 +128,7 @@ def get_results(self, validate: bool = True): "significance_code": significance_code, "expected_loss": loss, "variants": [asdict(variant) for variant in [control_variant, *test_variants]], + "credible_intervals": credible_intervals, } def get_variants(self, funnel_results): @@ -320,6 +325,43 @@ def calculate_probability_of_winning_for_each(variants: list[Variant]) -> list[P return [max(0, 1 - total_test_probabilities), *probabilities[1:]] +def calculate_credible_intervals(variants, lower_bound=0.025, upper_bound=0.975): + """ + Calculate the Bayesian credible intervals for a list of variants. + If no lower/upper bound provided, the function calculates the 95% credible interval. + """ + intervals = {} + + for variant in variants: + try: + if variant.success_count < 0 or variant.failure_count < 0: + capture_exception( + Exception("Invalid variant success/failure count"), + { + "variant": variant.key, + "success_count": variant.success_count, + "failure_count": variant.failure_count, + }, + ) + return {} + + # Calculate the credible interval + # Laplace smoothing: we add 1 to alpha and beta to avoid division errors if either is zero + alpha = variant.success_count + 1 + beta = variant.failure_count + 1 + credible_interval = stats.beta.ppf([lower_bound, upper_bound], alpha, beta) + + intervals[variant.key] = (credible_interval[0], credible_interval[1]) + except Exception as e: + capture_exception( + Exception(f"Error calculating credible interval for variant {variant.key}"), + {"error": str(e)}, + ) + return {} + + return intervals + + def validate_event_variants(funnel_results, variants): errors = { ExperimentNoResultsErrorKeys.NO_EVENTS: True, diff --git a/ee/clickhouse/queries/experiments/test_experiment_result.py b/ee/clickhouse/queries/experiments/test_experiment_result.py index 18eb673bf9ac8..ebc1f1ddaacba 100644 --- a/ee/clickhouse/queries/experiments/test_experiment_result.py +++ b/ee/clickhouse/queries/experiments/test_experiment_result.py @@ -8,6 +8,7 @@ ClickhouseFunnelExperimentResult, Variant, calculate_expected_loss, + calculate_credible_intervals, ) from ee.clickhouse.queries.experiments.trend_experiment_result import ( ClickhouseTrendExperimentResult, @@ -162,6 +163,13 @@ def test_calculate_results(self): self.assertAlmostEqual(loss, 0.0016, places=3) self.assertEqual(significant, ExperimentSignificanceCode.SIGNIFICANT) + credible_intervals = calculate_credible_intervals([variant_control, variant_test]) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.7715, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.9010, places=3) + self.assertAlmostEqual(credible_intervals[variant_test.key][0], 0.8405, places=3) + self.assertAlmostEqual(credible_intervals[variant_test.key][1], 0.9494, places=3) + def test_simulation_result_is_close_to_closed_form_solution(self): variant_test = Variant("A", 100, 10) variant_control = Variant("B", 100, 18) @@ -174,8 +182,8 @@ def test_simulation_result_is_close_to_closed_form_solution(self): def test_calculate_results_for_two_test_variants(self): variant_test_1 = Variant("A", 100, 10) - variant_test_2 = Variant("A", 100, 3) - variant_control = Variant("B", 100, 18) + variant_test_2 = Variant("B", 100, 3) + variant_control = Variant("C", 100, 18) probabilities = ClickhouseFunnelExperimentResult.calculate_results( variant_control, [variant_test_1, variant_test_2] @@ -203,10 +211,19 @@ def test_calculate_results_for_two_test_variants(self): self.assertAlmostEqual(loss, 0.00000, places=3) self.assertEqual(significant, ExperimentSignificanceCode.SIGNIFICANT) + credible_intervals = calculate_credible_intervals([variant_control, variant_test_1, variant_test_2]) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.7715, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.9010, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][0], 0.8405, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][1], 0.9494, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][0], 0.9180, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][1], 0.9894, places=3) + def test_calculate_results_for_two_test_variants_almost_equal(self): variant_test_1 = Variant("A", 120, 60) - variant_test_2 = Variant("A", 110, 52) - variant_control = Variant("B", 130, 65) + variant_test_2 = Variant("B", 110, 52) + variant_control = Variant("C", 130, 65) probabilities = ClickhouseFunnelExperimentResult.calculate_results( variant_control, [variant_test_1, variant_test_2] @@ -233,6 +250,15 @@ def test_calculate_results_for_two_test_variants_almost_equal(self): self.assertAlmostEqual(loss, 1, places=3) self.assertEqual(significant, ExperimentSignificanceCode.LOW_WIN_PROBABILITY) + credible_intervals = calculate_credible_intervals([variant_control, variant_test_1, variant_test_2]) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.5977, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.7290, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][0], 0.5948, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][1], 0.7314, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][0], 0.6035, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][1], 0.7460, places=3) + def test_absolute_loss_less_than_one_percent_but_not_significant(self): variant_test_1 = Variant("A", 286, 2014) variant_control = Variant("B", 267, 2031) @@ -250,11 +276,18 @@ def test_absolute_loss_less_than_one_percent_but_not_significant(self): self.assertAlmostEqual(loss, 1, places=3) self.assertEqual(significant, ExperimentSignificanceCode.LOW_WIN_PROBABILITY) + credible_intervals = calculate_credible_intervals([variant_control, variant_test_1]) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.1037, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.1299, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][0], 0.1114, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][1], 0.1384, places=3) + def test_calculate_results_for_three_test_variants(self): variant_test_1 = Variant("A", 100, 10) - variant_test_2 = Variant("A", 100, 3) - variant_test_3 = Variant("A", 100, 30) - variant_control = Variant("B", 100, 18) + variant_test_2 = Variant("B", 100, 3) + variant_test_3 = Variant("C", 100, 30) + variant_control = Variant("D", 100, 18) probabilities = ClickhouseFunnelExperimentResult.calculate_results( variant_control, [variant_test_1, variant_test_2, variant_test_3] @@ -285,11 +318,24 @@ def test_calculate_results_for_three_test_variants(self): self.assertAlmostEqual(loss, 0.0004, places=2) self.assertEqual(significant, ExperimentSignificanceCode.SIGNIFICANT) + credible_intervals = calculate_credible_intervals( + [variant_control, variant_test_1, variant_test_2, variant_test_3] + ) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.7715, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.9010, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][0], 0.8405, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][1], 0.9494, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][0], 0.9180, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][1], 0.9894, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][0], 0.6894, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][1], 0.8332, places=3) + def test_calculate_results_for_three_test_variants_almost_equal(self): - variant_control = Variant("B", 130, 65) variant_test_1 = Variant("A", 120, 60) - variant_test_2 = Variant("A", 110, 52) - variant_test_3 = Variant("A", 100, 46) + variant_test_2 = Variant("B", 110, 52) + variant_test_3 = Variant("C", 100, 46) + variant_control = Variant("D", 130, 65) probabilities = ClickhouseFunnelExperimentResult.calculate_results( variant_control, [variant_test_1, variant_test_2, variant_test_3] @@ -318,11 +364,24 @@ def test_calculate_results_for_three_test_variants_almost_equal(self): self.assertAlmostEqual(loss, 0.012, places=2) self.assertEqual(significant, ExperimentSignificanceCode.HIGH_LOSS) + credible_intervals = calculate_credible_intervals( + [variant_control, variant_test_1, variant_test_2, variant_test_3] + ) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.5977, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.7290, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][0], 0.5948, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][1], 0.7314, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][0], 0.6035, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][1], 0.7460, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][0], 0.6054, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][1], 0.7547, places=3) + def test_calculate_results_for_three_test_variants_much_better_than_control(self): - variant_control = Variant("B", 80, 65) variant_test_1 = Variant("A", 130, 60) - variant_test_2 = Variant("A", 135, 62) - variant_test_3 = Variant("A", 132, 60) + variant_test_2 = Variant("B", 135, 62) + variant_test_3 = Variant("C", 132, 60) + variant_control = Variant("D", 80, 65) probabilities = ClickhouseFunnelExperimentResult.calculate_results( variant_control, [variant_test_1, variant_test_2, variant_test_3] @@ -342,15 +401,28 @@ def test_calculate_results_for_three_test_variants_much_better_than_control(self self.assertAlmostEqual(loss, 0, places=2) self.assertEqual(significant, ExperimentSignificanceCode.SIGNIFICANT) + credible_intervals = calculate_credible_intervals( + [variant_control, variant_test_1, variant_test_2, variant_test_3] + ) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.4703, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.6303, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][0], 0.6148, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][1], 0.7460, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][0], 0.6172, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][1], 0.7460, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][0], 0.6186, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][1], 0.7488, places=3) + def test_calculate_results_for_seven_test_variants(self): variant_test_1 = Variant("A", 100, 17) - variant_test_2 = Variant("A", 100, 16) - variant_test_3 = Variant("A", 100, 30) - variant_test_4 = Variant("A", 100, 31) - variant_test_5 = Variant("A", 100, 29) - variant_test_6 = Variant("A", 100, 32) - variant_test_7 = Variant("A", 100, 33) - variant_control = Variant("B", 100, 18) + variant_test_2 = Variant("B", 100, 16) + variant_test_3 = Variant("C", 100, 30) + variant_test_4 = Variant("D", 100, 31) + variant_test_5 = Variant("E", 100, 29) + variant_test_6 = Variant("F", 100, 32) + variant_test_7 = Variant("G", 100, 33) + variant_control = Variant("H", 100, 18) probabilities = ClickhouseFunnelExperimentResult.calculate_results( variant_control, @@ -407,6 +479,36 @@ def test_calculate_results_for_seven_test_variants(self): self.assertAlmostEqual(loss, 1, places=2) self.assertEqual(significant, ExperimentSignificanceCode.LOW_WIN_PROBABILITY) + credible_intervals = calculate_credible_intervals( + [ + variant_control, + variant_test_1, + variant_test_2, + variant_test_3, + variant_test_4, + variant_test_5, + variant_test_6, + variant_test_7, + ] + ) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.7715, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.9010, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][0], 0.7793, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][1], 0.9070, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][0], 0.7874, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][1], 0.9130, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][0], 0.6894, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][1], 0.8332, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_4.key][0], 0.6835, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_4.key][1], 0.8278, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_5.key][0], 0.6955, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_5.key][1], 0.8385, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_6.key][0], 0.6776, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_6.key][1], 0.8226, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_7.key][0], 0.6718, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_7.key][1], 0.8174, places=3) + def test_calculate_results_control_is_significant(self): variant_test = Variant("test", 100, 18) variant_control = Variant("control", 100, 10) @@ -422,6 +524,13 @@ def test_calculate_results_control_is_significant(self): self.assertAlmostEqual(loss, 0.0016, places=3) self.assertEqual(significant, ExperimentSignificanceCode.SIGNIFICANT) + credible_intervals = calculate_credible_intervals([variant_control, variant_test]) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.8405, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.9494, places=3) + self.assertAlmostEqual(credible_intervals[variant_test.key][0], 0.7715, places=3) + self.assertAlmostEqual(credible_intervals[variant_test.key][1], 0.9010, places=3) + def test_calculate_results_many_variants_control_is_significant(self): variant_test_1 = Variant("test_1", 100, 20) variant_test_2 = Variant("test_2", 100, 21) @@ -451,6 +560,33 @@ def test_calculate_results_many_variants_control_is_significant(self): self.assertAlmostEqual(loss, 0.0008, places=3) self.assertEqual(significant, ExperimentSignificanceCode.SIGNIFICANT) + credible_intervals = calculate_credible_intervals( + [ + variant_control, + variant_test_1, + variant_test_2, + variant_test_3, + variant_test_4, + variant_test_5, + variant_test_6, + ] + ) + # Cross-checked with: https://www.causascientia.org/math_stat/ProportionCI.html + self.assertAlmostEqual(credible_intervals[variant_control.key][0], 0.8405, places=3) + self.assertAlmostEqual(credible_intervals[variant_control.key][1], 0.9494, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][0], 0.7563, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_1.key][1], 0.8892, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][0], 0.7489, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_2.key][1], 0.8834, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][0], 0.7418, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_3.key][1], 0.8776, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_4.key][0], 0.7347, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_4.key][1], 0.8718, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_5.key][0], 0.7279, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_5.key][1], 0.8661, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_6.key][0], 0.7211, places=3) + self.assertAlmostEqual(credible_intervals[variant_test_6.key][1], 0.8605, places=3) + # calculation: https://www.evanmiller.org/bayesian-ab-testing.html#count_ab def calculate_probability_of_winning_for_target_count_data( diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr index b38a8ebc418f2..b9dc9a44b8c58 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr @@ -184,186 +184,3 @@ GROUP BY prop ''' # --- -# name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results.6 - ''' - /* user_id:0 request:_snapshot_ */ - SELECT countIf(steps = 1) step_1, - countIf(steps = 2) step_2, - avg(step_1_average_conversion_time_inner) step_1_average_conversion_time, - median(step_1_median_conversion_time_inner) step_1_median_conversion_time, - prop - FROM - (SELECT aggregation_target, - steps, - avg(step_1_conversion_time) step_1_average_conversion_time_inner, - median(step_1_conversion_time) step_1_median_conversion_time_inner , - prop - FROM - (SELECT aggregation_target, - steps, - max(steps) over (PARTITION BY aggregation_target, - prop) as max_steps, - step_1_conversion_time , - prop - FROM - (SELECT *, - if(latest_0 <= latest_1 - AND latest_1 <= latest_0 + INTERVAL 14 DAY, 2, 1) AS steps , - if(isNotNull(latest_1) - AND latest_1 <= latest_0 + INTERVAL 14 DAY, dateDiff('second', toDateTime(latest_0), toDateTime(latest_1)), NULL) step_1_conversion_time, - prop - FROM - (SELECT aggregation_target, timestamp, step_0, - latest_0, - step_1, - min(latest_1) over (PARTITION by aggregation_target, - prop - ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) latest_1 , - if(has([['test'], ['control'], ['']], prop), prop, ['Other']) as prop - FROM - (SELECT *, - if(notEmpty(arrayFilter(x -> notEmpty(x), prop_vals)), prop_vals, ['']) as prop - FROM - (SELECT e.timestamp as timestamp, - pdi.person_id as aggregation_target, - pdi.person_id as person_id, - if(event = '$pageview_funnel', 1, 0) as step_0, - if(step_0 = 1, timestamp, null) as latest_0, - if(event = '$pageleave_funnel', 1, 0) as step_1, - if(step_1 = 1, timestamp, null) as latest_1, - array(replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '')) AS prop_basic, - prop_basic as prop, - argMinIf(prop, timestamp, notEmpty(arrayFilter(x -> notEmpty(x), prop))) over (PARTITION by aggregation_target) as prop_vals - FROM events e - INNER JOIN - (SELECT distinct_id, - argMax(person_id, version) as person_id - FROM person_distinct_id2 - WHERE team_id = 2 - AND distinct_id IN - (SELECT distinct_id - FROM events - WHERE team_id = 2 - AND event IN ['$pageleave_funnel', '$pageview_funnel'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2020-01-01 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') ) - GROUP BY distinct_id - HAVING argMax(is_deleted, version) = 0) AS pdi ON e.distinct_id = pdi.distinct_id - WHERE team_id = 2 - AND event IN ['$pageleave_funnel', '$pageview_funnel'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2020-01-01 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') - AND (step_0 = 1 - OR step_1 = 1) ))) - WHERE step_0 = 1 )) - GROUP BY aggregation_target, - steps, - prop - HAVING steps = max_steps) - GROUP BY prop - ''' -# --- -# name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results.7 - ''' - /* user_id:0 request:_snapshot_ */ - SELECT [now()] AS date, - [0] AS total, - '' AS breakdown_value - LIMIT 0 - ''' -# --- -# name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results.8 - ''' - /* user_id:0 request:_snapshot_ */ - SELECT array(replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '')) AS value, - count(*) as count - FROM events e - WHERE team_id = 2 - AND event IN ['$pageleave_funnel', '$pageview_funnel'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2020-01-01 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') - GROUP BY value - ORDER BY count DESC, value DESC - LIMIT 26 - OFFSET 0 - ''' -# --- -# name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results.9 - ''' - /* user_id:0 request:_snapshot_ */ - SELECT countIf(steps = 1) step_1, - countIf(steps = 2) step_2, - avg(step_1_average_conversion_time_inner) step_1_average_conversion_time, - median(step_1_median_conversion_time_inner) step_1_median_conversion_time, - prop - FROM - (SELECT aggregation_target, - steps, - avg(step_1_conversion_time) step_1_average_conversion_time_inner, - median(step_1_conversion_time) step_1_median_conversion_time_inner , - prop - FROM - (SELECT aggregation_target, - steps, - max(steps) over (PARTITION BY aggregation_target, - prop) as max_steps, - step_1_conversion_time , - prop - FROM - (SELECT *, - if(latest_0 <= latest_1 - AND latest_1 <= latest_0 + INTERVAL 14 DAY, 2, 1) AS steps , - if(isNotNull(latest_1) - AND latest_1 <= latest_0 + INTERVAL 14 DAY, dateDiff('second', toDateTime(latest_0), toDateTime(latest_1)), NULL) step_1_conversion_time, - prop - FROM - (SELECT aggregation_target, timestamp, step_0, - latest_0, - step_1, - min(latest_1) over (PARTITION by aggregation_target, - prop - ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) latest_1 , - if(has([['test'], ['control'], ['']], prop), prop, ['Other']) as prop - FROM - (SELECT *, - if(notEmpty(arrayFilter(x -> notEmpty(x), prop_vals)), prop_vals, ['']) as prop - FROM - (SELECT e.timestamp as timestamp, - pdi.person_id as aggregation_target, - pdi.person_id as person_id, - if(event = '$pageview_funnel', 1, 0) as step_0, - if(step_0 = 1, timestamp, null) as latest_0, - if(event = '$pageleave_funnel', 1, 0) as step_1, - if(step_1 = 1, timestamp, null) as latest_1, - array(replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '')) AS prop_basic, - prop_basic as prop, - argMinIf(prop, timestamp, notEmpty(arrayFilter(x -> notEmpty(x), prop))) over (PARTITION by aggregation_target) as prop_vals - FROM events e - INNER JOIN - (SELECT distinct_id, - argMax(person_id, version) as person_id - FROM person_distinct_id2 - WHERE team_id = 2 - AND distinct_id IN - (SELECT distinct_id - FROM events - WHERE team_id = 2 - AND event IN ['$pageleave_funnel', '$pageview_funnel'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2020-01-01 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') ) - GROUP BY distinct_id - HAVING argMax(is_deleted, version) = 0) AS pdi ON e.distinct_id = pdi.distinct_id - WHERE team_id = 2 - AND event IN ['$pageleave_funnel', '$pageview_funnel'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2020-01-01 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') - AND (step_0 = 1 - OR step_1 = 1) ))) - WHERE step_0 = 1 )) - GROUP BY aggregation_target, - steps, - prop - HAVING steps = max_steps) - GROUP BY prop - ''' -# --- diff --git a/ee/clickhouse/views/test/test_clickhouse_experiment_secondary_results.py b/ee/clickhouse/views/test/test_clickhouse_experiment_secondary_results.py index e7f9ebf7e2c3e..63f82db4a72ba 100644 --- a/ee/clickhouse/views/test/test_clickhouse_experiment_secondary_results.py +++ b/ee/clickhouse/views/test/test_clickhouse_experiment_secondary_results.py @@ -1167,6 +1167,7 @@ def test_metrics_without_full_flag_information_are_valid(self): "significant", "significance_code", "expected_loss", + "credible_intervals", "variants", } diff --git a/frontend/__snapshots__/components-subscriptions--subscription-no-integrations--dark.png b/frontend/__snapshots__/components-subscriptions--subscription-no-integrations--dark.png index dfb9726638586..575dd6ab01386 100644 Binary files a/frontend/__snapshots__/components-subscriptions--subscription-no-integrations--dark.png and b/frontend/__snapshots__/components-subscriptions--subscription-no-integrations--dark.png differ diff --git a/frontend/__snapshots__/components-subscriptions--subscription-no-integrations--light.png b/frontend/__snapshots__/components-subscriptions--subscription-no-integrations--light.png index f8a3a66ceeb79..23be08f1222f8 100644 Binary files a/frontend/__snapshots__/components-subscriptions--subscription-no-integrations--light.png and b/frontend/__snapshots__/components-subscriptions--subscription-no-integrations--light.png differ diff --git a/frontend/__snapshots__/components-subscriptions--subscriptions-edit--dark.png b/frontend/__snapshots__/components-subscriptions--subscriptions-edit--dark.png index a6ee55477f9dd..ab408ff6f720d 100644 Binary files a/frontend/__snapshots__/components-subscriptions--subscriptions-edit--dark.png and b/frontend/__snapshots__/components-subscriptions--subscriptions-edit--dark.png differ diff --git a/frontend/__snapshots__/components-subscriptions--subscriptions-edit--light.png b/frontend/__snapshots__/components-subscriptions--subscriptions-edit--light.png index 9a23e934bfb24..155376c9316b8 100644 Binary files a/frontend/__snapshots__/components-subscriptions--subscriptions-edit--light.png and b/frontend/__snapshots__/components-subscriptions--subscriptions-edit--light.png differ diff --git a/frontend/__snapshots__/components-subscriptions--subscriptions-new--dark.png b/frontend/__snapshots__/components-subscriptions--subscriptions-new--dark.png index e56c93615831c..41eb25aa83591 100644 Binary files a/frontend/__snapshots__/components-subscriptions--subscriptions-new--dark.png and b/frontend/__snapshots__/components-subscriptions--subscriptions-new--dark.png differ diff --git a/frontend/__snapshots__/components-subscriptions--subscriptions-new--light.png b/frontend/__snapshots__/components-subscriptions--subscriptions-new--light.png index 7c90b32c368e2..5f2a5ddc6eef3 100644 Binary files a/frontend/__snapshots__/components-subscriptions--subscriptions-new--light.png and b/frontend/__snapshots__/components-subscriptions--subscriptions-new--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--dark.png b/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--dark.png index c1cbe1b313f60..5ef2bd9c94e10 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--light.png b/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--light.png index b8e06ecbf1202..8e192d169ba4f 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--light.png and b/frontend/__snapshots__/lemon-ui-lemon-field--fields-with-kea-form--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--dark.png b/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--dark.png index a66f265ec591a..24eac264804e4 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--light.png b/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--light.png index 0fff1a35ec624..df046c562eb4f 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--light.png and b/frontend/__snapshots__/lemon-ui-lemon-field--pure-fields--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lettermark--extra-large--dark.png b/frontend/__snapshots__/lemon-ui-lettermark--extra-large--dark.png new file mode 100644 index 0000000000000..02daa03116041 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lettermark--extra-large--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lettermark--extra-large--light.png b/frontend/__snapshots__/lemon-ui-lettermark--extra-large--light.png new file mode 100644 index 0000000000000..cad8c2549a631 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lettermark--extra-large--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-lettermark--extra-small--dark.png b/frontend/__snapshots__/lemon-ui-lettermark--extra-small--dark.png new file mode 100644 index 0000000000000..1504a539f2f16 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lettermark--extra-small--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lettermark--extra-small--light.png b/frontend/__snapshots__/lemon-ui-lettermark--extra-small--light.png new file mode 100644 index 0000000000000..796f6e0d36822 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lettermark--extra-small--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--base--dark.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--base--dark.png new file mode 100644 index 0000000000000..8208d7dc199e0 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--base--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--base--light.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--base--light.png new file mode 100644 index 0000000000000..ae96aad0d36bc Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--base--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--base-without-media--dark.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--base-without-media--dark.png new file mode 100644 index 0000000000000..d91630fa0dd9f Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--base-without-media--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--base-without-media--light.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--base-without-media--light.png new file mode 100644 index 0000000000000..77dfacf352b8e Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--base-without-media--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large--dark.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large--dark.png new file mode 100644 index 0000000000000..0ee6bb9dbab8e Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large--light.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large--light.png new file mode 100644 index 0000000000000..dc9a4f64f65f8 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large-without-media--dark.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large-without-media--dark.png new file mode 100644 index 0000000000000..efef7a39e0aab Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large-without-media--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large-without-media--light.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large-without-media--light.png new file mode 100644 index 0000000000000..b2ebf3632d746 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-large-without-media--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small--dark.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small--dark.png new file mode 100644 index 0000000000000..68099b4545142 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small--light.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small--light.png new file mode 100644 index 0000000000000..f4d9eb863bdc6 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small-without-media--dark.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small-without-media--dark.png new file mode 100644 index 0000000000000..4b358e5633edf Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small-without-media--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small-without-media--light.png b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small-without-media--light.png new file mode 100644 index 0000000000000..d0da17c88ce6a Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-uploaded-logo--extra-small-without-media--light.png differ diff --git a/frontend/__snapshots__/posthog-3000-navigation--navigation-3000--dark.png b/frontend/__snapshots__/posthog-3000-navigation--navigation-3000--dark.png index 7196a9b39ee53..030f1724e0f3e 100644 Binary files a/frontend/__snapshots__/posthog-3000-navigation--navigation-3000--dark.png and b/frontend/__snapshots__/posthog-3000-navigation--navigation-3000--dark.png differ diff --git a/frontend/__snapshots__/replay-listings--recordings-play-lists--dark.png b/frontend/__snapshots__/replay-listings--recordings-play-lists--dark.png index 33960aa7f63d8..40382d1686a74 100644 Binary files a/frontend/__snapshots__/replay-listings--recordings-play-lists--dark.png and b/frontend/__snapshots__/replay-listings--recordings-play-lists--dark.png differ diff --git a/frontend/__snapshots__/replay-listings--recordings-play-lists--light.png b/frontend/__snapshots__/replay-listings--recordings-play-lists--light.png index 554a7378f9f71..91910cdea3e18 100644 Binary files a/frontend/__snapshots__/replay-listings--recordings-play-lists--light.png and b/frontend/__snapshots__/replay-listings--recordings-play-lists--light.png differ diff --git a/frontend/__snapshots__/replay-player-success--recordings-play-list-no-pinned-recordings--dark.png b/frontend/__snapshots__/replay-player-success--recordings-play-list-no-pinned-recordings--dark.png index 7f91d47111807..7fdf9df817e35 100644 Binary files a/frontend/__snapshots__/replay-player-success--recordings-play-list-no-pinned-recordings--dark.png and b/frontend/__snapshots__/replay-player-success--recordings-play-list-no-pinned-recordings--dark.png differ diff --git a/frontend/__snapshots__/replay-player-success--recordings-play-list-no-pinned-recordings--light.png b/frontend/__snapshots__/replay-player-success--recordings-play-list-no-pinned-recordings--light.png index a8a58c75d714e..6b12e17b93cb0 100644 Binary files a/frontend/__snapshots__/replay-player-success--recordings-play-list-no-pinned-recordings--light.png and b/frontend/__snapshots__/replay-player-success--recordings-play-list-no-pinned-recordings--light.png differ diff --git a/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--dark.png b/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--dark.png index 7f91d47111807..7fdf9df817e35 100644 Binary files a/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--dark.png and b/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--dark.png differ diff --git a/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--light.png b/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--light.png index 22110547d5591..6b12e17b93cb0 100644 Binary files a/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--light.png and b/frontend/__snapshots__/replay-player-success--recordings-play-list-with-pinned-recordings--light.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--new-select-variables--dark.png b/frontend/__snapshots__/scenes-app-dashboards--new-select-variables--dark.png index 61c133fd51f2b..d1a1c7420202e 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--new-select-variables--dark.png and b/frontend/__snapshots__/scenes-app-dashboards--new-select-variables--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--new-select-variables--light.png b/frontend/__snapshots__/scenes-app-dashboards--new-select-variables--light.png index f1a471957290e..2accb22d52831 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--new-select-variables--light.png and b/frontend/__snapshots__/scenes-app-dashboards--new-select-variables--light.png differ diff --git a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--dark.png b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--dark.png index f1e9cc5f96fea..5abc4050d02c5 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--dark.png and b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--light.png b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--light.png index 0370f8bb53c4f..cfe7c23a46517 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--light.png and b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--light.png differ diff --git a/frontend/__snapshots__/scenes-app-data-management-actions--actions-list--dark.png b/frontend/__snapshots__/scenes-app-data-management-actions--actions-list--dark.png index d7d59acfcaa1e..f62a42ff859d1 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management-actions--actions-list--dark.png and b/frontend/__snapshots__/scenes-app-data-management-actions--actions-list--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-data-management-actions--actions-list--light.png b/frontend/__snapshots__/scenes-app-data-management-actions--actions-list--light.png index d3a3176735af1..3aefe218922c0 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management-actions--actions-list--light.png and b/frontend/__snapshots__/scenes-app-data-management-actions--actions-list--light.png differ diff --git a/frontend/__snapshots__/scenes-app-errortracking--group-page--light.png b/frontend/__snapshots__/scenes-app-errortracking--group-page--light.png index 904f485ef38ed..52977a1b90d21 100644 Binary files a/frontend/__snapshots__/scenes-app-errortracking--group-page--light.png and b/frontend/__snapshots__/scenes-app-errortracking--group-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-errortracking--list-page--dark.png b/frontend/__snapshots__/scenes-app-errortracking--list-page--dark.png index 2b3fa4c87ab6f..749b83b991379 100644 Binary files a/frontend/__snapshots__/scenes-app-errortracking--list-page--dark.png and b/frontend/__snapshots__/scenes-app-errortracking--list-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-errortracking--list-page--light.png b/frontend/__snapshots__/scenes-app-errortracking--list-page--light.png index ba71e2e6f4519..afe1e2e61eb18 100644 Binary files a/frontend/__snapshots__/scenes-app-errortracking--list-page--light.png and b/frontend/__snapshots__/scenes-app-errortracking--list-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--experiments-list--dark.png b/frontend/__snapshots__/scenes-app-experiments--experiments-list--dark.png index bd0be08aa8149..9944485ce6c2f 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--experiments-list--dark.png and b/frontend/__snapshots__/scenes-app-experiments--experiments-list--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--experiments-list--light.png b/frontend/__snapshots__/scenes-app-experiments--experiments-list--light.png index a5aac2274f3df..def9e9eeb9007 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--experiments-list--light.png and b/frontend/__snapshots__/scenes-app-experiments--experiments-list--light.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--dark.png b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--dark.png index 2d5cf6f454b12..aba51dfd51ab1 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--dark.png and b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--light.png b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--light.png index 8d764acb665b8..8c0e31ba5fcab 100644 Binary files a/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--light.png and b/frontend/__snapshots__/scenes-app-feature-flags--new-feature-flag--light.png differ diff --git a/frontend/__snapshots__/scenes-app-features--new-feature-flag--dark.png b/frontend/__snapshots__/scenes-app-features--new-feature-flag--dark.png index 289125a6adfde..81745db4c21a6 100644 Binary files a/frontend/__snapshots__/scenes-app-features--new-feature-flag--dark.png and b/frontend/__snapshots__/scenes-app-features--new-feature-flag--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-features--new-feature-flag--light.png b/frontend/__snapshots__/scenes-app-features--new-feature-flag--light.png index 212cc93075af2..0de927df79ea6 100644 Binary files a/frontend/__snapshots__/scenes-app-features--new-feature-flag--light.png and b/frontend/__snapshots__/scenes-app-features--new-feature-flag--light.png differ diff --git a/frontend/__snapshots__/scenes-app-heatmaps--heatmaps-browser-no-pages-available--light.png b/frontend/__snapshots__/scenes-app-heatmaps--heatmaps-browser-no-pages-available--light.png index bf252fac23b48..26f973266a96a 100644 Binary files a/frontend/__snapshots__/scenes-app-heatmaps--heatmaps-browser-no-pages-available--light.png and b/frontend/__snapshots__/scenes-app-heatmaps--heatmaps-browser-no-pages-available--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark--webkit.png index 286c9c38a51ff..59056a8eb9ac8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark.png index 935d7f59e328d..306ba0e495da6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light--webkit.png index 33cc713380214..dd59e19513cda 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light.png index e296d99ef8cb1..49e2ee54931e3 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--dark--webkit.png index 01abe2337c5c2..11b317729f12c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--light--webkit.png index 08ad996244cf1..7adeb0f141b14 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark--webkit.png index 1654fbd0434eb..6c69dab1c84ad 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png index 92510f386201d..e1f8a6876c48f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png index 38458f7c76eab..458057bd09154 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png index c4f4ef1b521b2..c73cfc49f75ac 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark--webkit.png index c483b451ab1b4..1316c351a352e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png index 765ca1197abd4..667f5c839eea3 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light--webkit.png index ecedf5d561eed..211750fcd7ac0 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light.png index 27a04cf8778b8..8ea76b05fa49e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark--webkit.png index b96cb06249bf4..25a8084741abc 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark.png index bdf9aef23d57a..ab01c22dd51e3 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light--webkit.png index d9059836e0023..36d3a6c1f64d0 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light.png index 07ebdf53420b2..2fd407b6bb1d9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png index 756c64885336f..cb1f92445b96e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark--webkit.png index 0854a7d9d59e4..99c37ddd6eb77 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png index c913398e0eea4..3b65c7a67f96f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png index 64081b6d66734..71a799da7b06f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png index f49a4a540b442..c628ae564cb22 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark--webkit.png index d0af757c17d12..3e578db24114e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark.png index f91ad4cc545c9..9b8b231585db1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light--webkit.png index b6d0dd0299c8e..5d33171388748 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png index a41d2e15134d2..ed2989ab6bc20 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark--webkit.png index b9e24f1a067e4..40e8e445ae0d3 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark.png index e85dcfd0fc186..27f50c18509b6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light--webkit.png index 306560d7fe31b..4cd9581bb3419 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light.png index 4479ec32fce48..4659907beec5c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--retention-edit--dark.png index 7cc486d16d70c..bf76fde0ae9fa 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--retention-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness--dark--webkit.png index 4dddc03792fbf..27b1da05b82fe 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark--webkit.png index 24513bc97d52b..68fe5da61bada 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark.png index c8a4259e31b45..a9766fb32f02c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light--webkit.png index a9cb79a405a60..61dad0d0766e4 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light.png index 10bcfd99d28bc..243c1cea204e1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark--webkit.png index 703f2d5505f6c..7b63b82b38ac8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark.png index 3a33e62e9f183..d744bbbdc7652 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light--webkit.png index 5c420e1cfb197..0c2234041c9f7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light.png index 8a3d586dec518..34b94449ec269 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark--webkit.png index f001930bc2f26..48fa25414102c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark.png index a465cc83c938b..2e8dab4d44d55 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light--webkit.png index 84eecb912e636..dd1c927283f44 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light.png index dc735b8c6a63f..378d63a5e671d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar--dark--webkit.png index 14c27f43376af..0bd4d174a0878 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-bar--dark.png index bfffdb48a5edd..a7275ad1398f5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar--light--webkit.png index 2572230cfa23f..ec47d8e18b66c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar--light.png b/frontend/__snapshots__/scenes-app-insights--trends-bar--light.png index 5091c7e91ba08..f7db36ad504ca 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png index 9ecc6eb502630..64d99b712d842 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png index 8772794b6cf0e..39fab033ecbe3 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png index de489230f4373..2f87102a89afe 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png index 35c63b9d30b92..57a0cc2eaca3c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png index b913de150fbd1..55bf81ce188f4 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png index aaa2db306520f..a68f62c85b33f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png index e8f9a514f4c77..4dcd20b4b0385 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png index 812cc30a047a2..fab546748b274 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line--dark--webkit.png index 944162d8cbb9c..f860266bb8b70 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-line--dark.png index d364433ea2c43..bd4f80e2ffe1b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-line--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line--light--webkit.png index c8b65fe1afd17..e4dc6b2d92e95 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line--light.png index 65997553f6135..c2c88afbe9a28 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png index bf1e388bbb3a3..e5b89d0605c8e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png index 369f2b96ff417..a60f02762ba2c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png index c4a97cf4320ae..7679dbac61976 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png index 520ff4c42811a..c386f35d6f590 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark--webkit.png index 0eb8180a4a2c7..53be7b7925e3c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark.png index 4f3085e9885e4..a8ce5c1b1e0ca 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light--webkit.png index a0e65dc0de6ed..b38003b8b2c36 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light.png index 6ff7c747b028f..08b032d49ce97 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark--webkit.png index b72057870a61c..3ced4b9044f5e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark.png index a797bef6bd8f0..bb98a8a0690d8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light--webkit.png index 506e6efe66343..7314a7a3c3115 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light.png index 9518b19db1942..08ada10442d45 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark--webkit.png index 1692b49b6ed36..d2b1aaef7eea2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark.png index 2e1f561de2e38..41d9c9a163cb7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light--webkit.png index c008e63d38744..fda65abc4fae2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light.png index 7ac2ab373e681..d71a92f7c8390 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-pie--dark.png index 2845b3d184420..b8cba735d457c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie--light--webkit.png index 7225389ec8c8f..8286196b8b814 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie--light.png b/frontend/__snapshots__/scenes-app-insights--trends-pie--light.png index cacd8d5a6cbf7..ca0b31106f665 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png index 9c4906241cb5c..f84ffcfeca674 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png index 76fc76f512bb4..a270542e3d5a3 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png index a036845499191..2b3e77629b9ff 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png index 56e46271a6c68..560130a8d43fa 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png index 9fc310a03c735..bde049dababeb 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png index 64cc95e689d4c..ce2846cd04108 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png index 30973b2dfe9d5..329801d59fc36 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png index beda31e351f04..165d63d09a6b5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png index bbef1dc641e31..6297f2c69a582 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png index 6d00abb68cb01..51455cc26454b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png index a33d36de75ce6..c978cd931b563 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png index 8977f01095e89..8895efc444db6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png index e7b2046407826..85215ff16f1f6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png index 78299573b0dd4..bb7854e2370b3 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png index 4a4347f456f30..dfb5eb142c7e7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png index df125d265b5ef..4b2947dadc3c0 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark--webkit.png index 80d05ae6ea9c1..f3a432a33b32e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark.png index 1c21bb7ee74ad..6d21a49ae1d93 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light--webkit.png index 2b285b08ba12e..c5a68841ec463 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light.png index 9bcafe36b954e..a8178333d0764 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark--webkit.png index 6b70cd964ab63..9d0916989515a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark.png index f407a20c724ef..dd791f4e9e27b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light--webkit.png index 604b86eeb9717..56160ec991fc9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light.png index 8b6ac74df2919..aba24e56a0b36 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png index 84ca5279a4031..f9f8cab5d9f5b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png index 006de0f37ddc9..2364c65982396 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png index 91c6c22f43901..91bc9fe36a76a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png index 1925612432f81..ce9a1effb18df 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark--webkit.png index 67f62a0f43d5d..e01a343fd408e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark.png index c4e507334aaaf..c6debe0d33136 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png index 7351a29f7e2c1..772afd26a8869 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png index fa0b96118b5a5..ca1cea1ec5a32 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights-error-empty-states--empty--dark.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--empty--dark.png index 1b19ae99893e4..2bb4ceb2568ca 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--empty--dark.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--empty--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights-error-empty-states--empty--light.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--empty--light.png index 447d3b5e0b03f..84c274c08f8da 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--empty--light.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--empty--light.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 88eba9720fc31..0585b1bdd1170 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 f2b71e0d1e50a..b28e6f1c23f02 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-insights-error-empty-states--server-error--dark.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--server-error--dark.png index 88eba9720fc31..0585b1bdd1170 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--server-error--dark.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--server-error--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights-error-empty-states--server-error--light.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--server-error--light.png index f2b71e0d1e50a..b28e6f1c23f02 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--server-error--light.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--server-error--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights-error-empty-states--validation-error--dark.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--validation-error--dark.png index 88eba9720fc31..0585b1bdd1170 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--validation-error--dark.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--validation-error--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights-error-empty-states--validation-error--light.png b/frontend/__snapshots__/scenes-app-insights-error-empty-states--validation-error--light.png index f2b71e0d1e50a..b28e6f1c23f02 100644 Binary files a/frontend/__snapshots__/scenes-app-insights-error-empty-states--validation-error--light.png and b/frontend/__snapshots__/scenes-app-insights-error-empty-states--validation-error--light.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--dark.png b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--dark.png index 08adf8e9b2dc7..fffa2f979f6c1 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--dark.png and b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-legacy-sources-page--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-legacy-sources-page--dark.png index e9fd3fecba0f8..ed6f9ea93f0b2 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-legacy-sources-page--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-legacy-sources-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-legacy-sources-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-legacy-sources-page--light.png index ff4647ba15841..3517876810712 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-legacy-sources-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-legacy-sources-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-configuration-404--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-configuration-404--dark.png index 6aaf0455f196b..d74b0566e4d2e 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-configuration-404--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-configuration-404--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-configuration-404--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-configuration-404--light.png index 805f67986f239..b328a1e0da8f9 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-configuration-404--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-configuration-404--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration--dark.png index 329c9fe9e985b..413130c6296c3 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration--light.png index acbb42b255d52..c033b0efd10ed 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration-stateless-plugin--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration-stateless-plugin--dark.png index e0045d56ba315..b9b95aeac4ee0 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration-stateless-plugin--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration-stateless-plugin--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration-stateless-plugin--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration-stateless-plugin--light.png index 7ed1ada26b511..9d67970304d0b 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration-stateless-plugin--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-edit-configuration-stateless-plugin--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--dark.png index d023908198ae1..e25c96644ddd2 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--light.png index 009d03f7e7dca..1a620b20794ec 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--dark.png index 2a5f9525df9b0..55e790617f8ae 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--light.png index 1ab4188bfbec0..f147e706f190b 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics--dark.png index d9f0e8a769817..0f1a6fdc7a713 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics--light.png index b46d77e19ddc0..0011ccfb90b5f 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics-error-modal--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics-error-modal--dark.png index 6674cb0965b99..4e6de62cf0cc0 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics-error-modal--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics-error-modal--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics-error-modal--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics-error-modal--light.png index 27ba6ab63af1b..a5548f8d50580 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics-error-modal--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-metrics-error-modal--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query--dark.png index 769862b132a8d..faa022c44503d 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query--light.png index 4a112d3297ea8..4bbc0c68690a6 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query-without-pipelines--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query-without-pipelines--dark.png index 31830927d03a7..1b08684592918 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query-without-pipelines--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query-without-pipelines--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query-without-pipelines--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query-without-pipelines--light.png index db57af8e6d688..e996db8f7ad89 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query-without-pipelines--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-big-query-without-pipelines--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-sequence-timer--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-sequence-timer--dark.png index 76efcf9306caf..46fcf41e5b2a1 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-sequence-timer--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-sequence-timer--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-sequence-timer--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-sequence-timer--light.png index 64a999c5b875c..ded9697cc1999 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-sequence-timer--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-sequence-timer--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-transformation--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-transformation--dark.png index e9960b1add5e0..5b1f958a578a2 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-transformation--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-transformation--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-transformation--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-transformation--light.png index f299037020a0a..880e0e36b88d5 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-transformation--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-transformation--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-site-apps-page--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-site-apps-page--dark.png index c6a34732d99de..03a13f259dc88 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-site-apps-page--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-site-apps-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-site-apps-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-site-apps-page--light.png index 22bfea0198dbd..bc377741cec77 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-site-apps-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-site-apps-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--dark.png index b570264fc4945..fa4064db680f4 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--light.png index 406bff91d7cd5..b25067d0f5243 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page--light.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--dark.png index ee4d0b5c98eed..ef04659976be3 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--dark.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--light.png index 8bda82edc5a70..335c55bc238fc 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-transformations-page-empty--light.png differ diff --git a/frontend/__snapshots__/scenes-app-saved-insights--empty-state--dark.png b/frontend/__snapshots__/scenes-app-saved-insights--empty-state--dark.png index f48a4db9810da..08ea72aaf5282 100644 Binary files a/frontend/__snapshots__/scenes-app-saved-insights--empty-state--dark.png and b/frontend/__snapshots__/scenes-app-saved-insights--empty-state--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-saved-insights--empty-state--light.png b/frontend/__snapshots__/scenes-app-saved-insights--empty-state--light.png index 52ee33bc4e57d..da7a40f60f5f2 100644 Binary files a/frontend/__snapshots__/scenes-app-saved-insights--empty-state--light.png and b/frontend/__snapshots__/scenes-app-saved-insights--empty-state--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 index fc05ce1feb142..7c17549d8403c 100644 Binary files a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-with-email--dark.png 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 index 956a538572fa0..ada2bbf44246d 100644 Binary files a/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-with-email--light.png and b/frontend/__snapshots__/scenes-app-sidepanels--side-panel-support-with-email--light.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-multi-question-survey-section--dark.png b/frontend/__snapshots__/scenes-app-surveys--new-multi-question-survey-section--dark.png index 492d427a07ee0..0a15706cb4e4d 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-multi-question-survey-section--dark.png and b/frontend/__snapshots__/scenes-app-surveys--new-multi-question-survey-section--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-multi-question-survey-section--light.png b/frontend/__snapshots__/scenes-app-surveys--new-multi-question-survey-section--light.png index 218688578385c..bc01fbb47e885 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-multi-question-survey-section--light.png and b/frontend/__snapshots__/scenes-app-surveys--new-multi-question-survey-section--light.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey--dark.png b/frontend/__snapshots__/scenes-app-surveys--new-survey--dark.png index 492d427a07ee0..0a15706cb4e4d 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey--dark.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey--light.png b/frontend/__snapshots__/scenes-app-surveys--new-survey--light.png index 218688578385c..bc01fbb47e885 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey--light.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey--light.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section--dark.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section--dark.png index a348e8dcde89a..f6e5a5b1440b3 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section--dark.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section--light.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section--light.png index 19383e3a469f0..6a1efb6bcdeb1 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section--light.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-appearance-section--light.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-customisation-section--dark.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-customisation-section--dark.png index 8fcabe583ba3e..6b4cd870cddf0 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-customisation-section--dark.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-customisation-section--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-customisation-section--light.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-customisation-section--light.png index 0142f39a11703..ef029b39d89c9 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-customisation-section--light.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-customisation-section--light.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-presentation-section--dark.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-presentation-section--dark.png index 68c3fd96a99eb..c527df4d343cf 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-presentation-section--dark.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-presentation-section--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-presentation-section--light.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-presentation-section--light.png index f146c068b7195..2c38b5d6d1433 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-presentation-section--light.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-presentation-section--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 a6e9a92aa98bb..3247e9ad09ec5 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 09154cf265c26..ce68f7f358081 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-app-surveys--new-survey-with-html-question-description--dark.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-with-html-question-description--dark.png index db25d27eecd74..7d7d2205e7bf9 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-with-html-question-description--dark.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-with-html-question-description--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-with-html-question-description--light.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-with-html-question-description--light.png index 54042bd0dc090..14cd78bf630dc 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-with-html-question-description--light.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-with-html-question-description--light.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-with-text-question-description-that-does-not-render-html--dark.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-with-text-question-description-that-does-not-render-html--dark.png index beb8b23df9d28..03c99a26ad3f4 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-with-text-question-description-that-does-not-render-html--dark.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-with-text-question-description-that-does-not-render-html--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey-with-text-question-description-that-does-not-render-html--light.png b/frontend/__snapshots__/scenes-app-surveys--new-survey-with-text-question-description-that-does-not-render-html--light.png index 8f2da59697416..c61b6af2f9573 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey-with-text-question-description-that-does-not-render-html--light.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey-with-text-question-description-that-does-not-render-html--light.png differ diff --git a/frontend/__snapshots__/scenes-other-invitesignup--cloud--dark.png b/frontend/__snapshots__/scenes-other-invitesignup--cloud--dark.png index 44e2d9d1526af..056277ca7de27 100644 Binary files a/frontend/__snapshots__/scenes-other-invitesignup--cloud--dark.png and b/frontend/__snapshots__/scenes-other-invitesignup--cloud--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-invitesignup--cloud--light.png b/frontend/__snapshots__/scenes-other-invitesignup--cloud--light.png index c56e9a0a29f0c..c94401b0612d8 100644 Binary files a/frontend/__snapshots__/scenes-other-invitesignup--cloud--light.png and b/frontend/__snapshots__/scenes-other-invitesignup--cloud--light.png differ diff --git a/frontend/__snapshots__/scenes-other-invitesignup--cloud-eu--dark.png b/frontend/__snapshots__/scenes-other-invitesignup--cloud-eu--dark.png index 615cc2337346c..9d8fd6b71b21a 100644 Binary files a/frontend/__snapshots__/scenes-other-invitesignup--cloud-eu--dark.png and b/frontend/__snapshots__/scenes-other-invitesignup--cloud-eu--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-invitesignup--cloud-eu--light.png b/frontend/__snapshots__/scenes-other-invitesignup--cloud-eu--light.png index c339b3d71e20b..2906dffb2439c 100644 Binary files a/frontend/__snapshots__/scenes-other-invitesignup--cloud-eu--light.png and b/frontend/__snapshots__/scenes-other-invitesignup--cloud-eu--light.png differ diff --git a/frontend/__snapshots__/scenes-other-invitesignup--self-hosted--dark.png b/frontend/__snapshots__/scenes-other-invitesignup--self-hosted--dark.png index 4f527785cfc83..2e48f5d32d617 100644 Binary files a/frontend/__snapshots__/scenes-other-invitesignup--self-hosted--dark.png and b/frontend/__snapshots__/scenes-other-invitesignup--self-hosted--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-invitesignup--self-hosted--light.png b/frontend/__snapshots__/scenes-other-invitesignup--self-hosted--light.png index 1ae0eaced1b5e..01d623a3aeac6 100644 Binary files a/frontend/__snapshots__/scenes-other-invitesignup--self-hosted--light.png and b/frontend/__snapshots__/scenes-other-invitesignup--self-hosted--light.png differ diff --git a/frontend/__snapshots__/scenes-other-login--cloud--dark.png b/frontend/__snapshots__/scenes-other-login--cloud--dark.png index 063d65290b9c8..248838906767f 100644 Binary files a/frontend/__snapshots__/scenes-other-login--cloud--dark.png and b/frontend/__snapshots__/scenes-other-login--cloud--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-login--cloud--light.png b/frontend/__snapshots__/scenes-other-login--cloud--light.png index 39bfda648c9e2..7af77d18f81f9 100644 Binary files a/frontend/__snapshots__/scenes-other-login--cloud--light.png and b/frontend/__snapshots__/scenes-other-login--cloud--light.png differ diff --git a/frontend/__snapshots__/scenes-other-login--cloud-eu--dark.png b/frontend/__snapshots__/scenes-other-login--cloud-eu--dark.png index 51b87c1745d98..e37d0c8785a81 100644 Binary files a/frontend/__snapshots__/scenes-other-login--cloud-eu--dark.png and b/frontend/__snapshots__/scenes-other-login--cloud-eu--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-login--cloud-eu--light.png b/frontend/__snapshots__/scenes-other-login--cloud-eu--light.png index 39cd1029e21ec..ed793f72cac25 100644 Binary files a/frontend/__snapshots__/scenes-other-login--cloud-eu--light.png and b/frontend/__snapshots__/scenes-other-login--cloud-eu--light.png differ diff --git a/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement--dark.png b/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement--dark.png index 1460ba536b255..a2c805c28ff92 100644 Binary files a/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement--dark.png and b/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement--light.png b/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement--light.png index d2d9939d68ab8..bc1232a0281c7 100644 Binary files a/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement--light.png and b/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement--light.png differ diff --git a/frontend/__snapshots__/scenes-other-login--second-factor--dark.png b/frontend/__snapshots__/scenes-other-login--second-factor--dark.png index 14fa32638e0f4..16c45697fe40d 100644 Binary files a/frontend/__snapshots__/scenes-other-login--second-factor--dark.png and b/frontend/__snapshots__/scenes-other-login--second-factor--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-login--second-factor--light.png b/frontend/__snapshots__/scenes-other-login--second-factor--light.png index c5f5bc5771518..7c18f6ae30245 100644 Binary files a/frontend/__snapshots__/scenes-other-login--second-factor--light.png and b/frontend/__snapshots__/scenes-other-login--second-factor--light.png differ diff --git a/frontend/__snapshots__/scenes-other-login--self-hosted--dark.png b/frontend/__snapshots__/scenes-other-login--self-hosted--dark.png index e378d8fdf4377..566f8a2eabdef 100644 Binary files a/frontend/__snapshots__/scenes-other-login--self-hosted--dark.png and b/frontend/__snapshots__/scenes-other-login--self-hosted--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-login--self-hosted--light.png b/frontend/__snapshots__/scenes-other-login--self-hosted--light.png index e76ac6e4bca0b..d637e93d27b8b 100644 Binary files a/frontend/__snapshots__/scenes-other-login--self-hosted--light.png and b/frontend/__snapshots__/scenes-other-login--self-hosted--light.png differ diff --git a/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml--dark.png b/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml--dark.png index ff8ae44e09f4c..892289ddb6d8b 100644 Binary files a/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml--dark.png and b/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml--light.png b/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml--light.png index c982a6bd9a779..9813390bffd22 100644 Binary files a/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml--light.png and b/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml--light.png differ diff --git a/frontend/__snapshots__/scenes-other-login--sso-error--dark.png b/frontend/__snapshots__/scenes-other-login--sso-error--dark.png index 7121ab5bec4cd..6027c7d1dc51e 100644 Binary files a/frontend/__snapshots__/scenes-other-login--sso-error--dark.png and b/frontend/__snapshots__/scenes-other-login--sso-error--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-login--sso-error--light.png b/frontend/__snapshots__/scenes-other-login--sso-error--light.png index ac803786a54fe..19c04b6abc295 100644 Binary files a/frontend/__snapshots__/scenes-other-login--sso-error--light.png and b/frontend/__snapshots__/scenes-other-login--sso-error--light.png differ diff --git a/frontend/__snapshots__/scenes-other-password-reset--initial--dark.png b/frontend/__snapshots__/scenes-other-password-reset--initial--dark.png index 0a840f71e19fc..408eba0889de1 100644 Binary files a/frontend/__snapshots__/scenes-other-password-reset--initial--dark.png and b/frontend/__snapshots__/scenes-other-password-reset--initial--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-password-reset--initial--light.png b/frontend/__snapshots__/scenes-other-password-reset--initial--light.png index 5fd2ef2084f4e..1932393155a38 100644 Binary files a/frontend/__snapshots__/scenes-other-password-reset--initial--light.png and b/frontend/__snapshots__/scenes-other-password-reset--initial--light.png differ diff --git a/frontend/__snapshots__/scenes-other-password-reset-complete--default--dark.png b/frontend/__snapshots__/scenes-other-password-reset-complete--default--dark.png index 00828beb6a04d..29ec06761bde1 100644 Binary files a/frontend/__snapshots__/scenes-other-password-reset-complete--default--dark.png and b/frontend/__snapshots__/scenes-other-password-reset-complete--default--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-password-reset-complete--default--light.png b/frontend/__snapshots__/scenes-other-password-reset-complete--default--light.png index 27726a07b1c4c..b491fd017c128 100644 Binary files a/frontend/__snapshots__/scenes-other-password-reset-complete--default--light.png and b/frontend/__snapshots__/scenes-other-password-reset-complete--default--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 c970ca8efab92..a1747158a0580 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 60821b3bed2bd..3833c717c425a 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-session-timeout-all-options--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png index 1f0d0a20bca0b..7fbbd90b966bd 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png index 4b79ecbf8ed6d..b050994d51ecf 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png index 6c37bb5c3cea4..2968881dd0f6d 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png index 2649fa1de0ded..aaeccae01012e 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-user--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-user--dark.png index f0cd62ad82324..105b30153eae0 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-user--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-user--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-user--light.png b/frontend/__snapshots__/scenes-other-settings--settings-user--light.png index f72c5ee5146c4..0a0cb2ed46c66 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-user--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-user--light.png differ diff --git a/frontend/__snapshots__/scenes-other-signup--cloud--dark.png b/frontend/__snapshots__/scenes-other-signup--cloud--dark.png index ff972d070f02e..0cfa9fa533849 100644 Binary files a/frontend/__snapshots__/scenes-other-signup--cloud--dark.png and b/frontend/__snapshots__/scenes-other-signup--cloud--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-signup--cloud--light.png b/frontend/__snapshots__/scenes-other-signup--cloud--light.png index 65a2aa3820bd5..5cb4069790f4a 100644 Binary files a/frontend/__snapshots__/scenes-other-signup--cloud--light.png and b/frontend/__snapshots__/scenes-other-signup--cloud--light.png differ diff --git a/frontend/__snapshots__/scenes-other-signup--self-hosted--dark.png b/frontend/__snapshots__/scenes-other-signup--self-hosted--dark.png index 8c257487b72ad..2c3c6fe44cffa 100644 Binary files a/frontend/__snapshots__/scenes-other-signup--self-hosted--dark.png and b/frontend/__snapshots__/scenes-other-signup--self-hosted--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-signup--self-hosted--light.png b/frontend/__snapshots__/scenes-other-signup--self-hosted--light.png index 3d19d0254899d..9ca0dd64a0c37 100644 Binary files a/frontend/__snapshots__/scenes-other-signup--self-hosted--light.png and b/frontend/__snapshots__/scenes-other-signup--self-hosted--light.png differ diff --git a/frontend/__snapshots__/scenes-other-signup--self-hosted-sso--dark.png b/frontend/__snapshots__/scenes-other-signup--self-hosted-sso--dark.png index 1a4d84c471674..0a2e9c5ca5bbe 100644 Binary files a/frontend/__snapshots__/scenes-other-signup--self-hosted-sso--dark.png and b/frontend/__snapshots__/scenes-other-signup--self-hosted-sso--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-signup--self-hosted-sso--light.png b/frontend/__snapshots__/scenes-other-signup--self-hosted-sso--light.png index 11e2bf1eda063..8f91f89b51099 100644 Binary files a/frontend/__snapshots__/scenes-other-signup--self-hosted-sso--light.png and b/frontend/__snapshots__/scenes-other-signup--self-hosted-sso--light.png differ diff --git a/frontend/src/layout/navigation-3000/components/TopBar.scss b/frontend/src/layout/navigation-3000/components/TopBar.scss index a557ca3ab1a12..1628b3135ea86 100644 --- a/frontend/src/layout/navigation-3000/components/TopBar.scss +++ b/frontend/src/layout/navigation-3000/components/TopBar.scss @@ -112,10 +112,6 @@ transition: opacity 200ms ease; } - > .Lettermark { - margin-right: 0.5rem; - } - > .LemonIcon { margin-left: 0.125rem; font-size: 1rem; diff --git a/frontend/src/layout/navigation-3000/components/TopBar.tsx b/frontend/src/layout/navigation-3000/components/TopBar.tsx index ac4a4e7d8a6fa..805e52ad04e74 100644 --- a/frontend/src/layout/navigation-3000/components/TopBar.tsx +++ b/frontend/src/layout/navigation-3000/components/TopBar.tsx @@ -127,7 +127,9 @@ function Breadcrumb({ breadcrumb, here, isOnboarding }: BreadcrumbProps): JSX.El const breadcrumbName = isOnboarding && here ? 'Onboarding' : (breadcrumb.name as string) let nameElement: JSX.Element - if (breadcrumb.name != null && breadcrumb.onRename) { + if (breadcrumb.symbol) { + nameElement = breadcrumb.symbol + } else if (breadcrumb.name != null && breadcrumb.onRename) { nameElement = ( {nameElement} - {breadcrumb.popover && } + {breadcrumb.popover && !breadcrumb.symbol && } ) diff --git a/frontend/src/layout/navigation-3000/navigationLogic.tsx b/frontend/src/layout/navigation-3000/navigationLogic.tsx index 1ac04f137a230..4745b998e9814 100644 --- a/frontend/src/layout/navigation-3000/navigationLogic.tsx +++ b/frontend/src/layout/navigation-3000/navigationLogic.tsx @@ -455,6 +455,7 @@ export const navigation3000Logic = kea([ label: 'Error tracking', icon: , to: urls.errorTracking(), + tag: 'alpha' as const, } : null, featureFlags[FEATURE_FLAGS.HEATMAPS_UI] diff --git a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx index 7a30782f25079..6f60730e48603 100644 --- a/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx +++ b/frontend/src/layout/navigation/Breadcrumbs/breadcrumbsLogic.tsx @@ -1,7 +1,7 @@ +import { Tooltip } from '@posthog/lemon-ui' import { actions, connect, kea, listeners, path, props, reducers, selectors } from 'kea' import { subscriptions } from 'kea-subscriptions' -import { Lettermark } from 'lib/lemon-ui/Lettermark' -import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' +import { UploadedLogo } from 'lib/lemon-ui/UploadedLogo/UploadedLogo' import { identifierToHuman, objectsEqual, stripHTTP } from 'lib/utils' import { organizationLogic } from 'scenes/organizationLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' @@ -83,25 +83,16 @@ export const breadcrumbsLogic = kea([ } else if (activeScene) { const sceneConfig = s.sceneConfig(state, props) return [{ name: sceneConfig?.name ?? identifierToHuman(activeScene), key: activeScene }] - } else { - return [] } + return [] }, ], (crumbs): Breadcrumb[] => crumbs, { equalityCheck: objectsEqual }, ], appBreadcrumbs: [ - (s) => [ - s.preflight, - s.sceneConfig, - s.activeScene, - s.user, - s.currentOrganization, - s.currentTeam, - s.otherOrganizations, - ], - (preflight, sceneConfig, activeScene, user, currentOrganization, currentTeam, otherOrganizations) => { + (s) => [s.preflight, s.sceneConfig, s.activeScene, s.user, s.currentOrganization, s.currentTeam], + (preflight, sceneConfig, activeScene, user, currentOrganization, currentTeam) => { const breadcrumbs: Breadcrumb[] = [] if (!activeScene || !sceneConfig) { return breadcrumbs @@ -114,7 +105,6 @@ export const breadcrumbsLogic = kea([ breadcrumbs.push({ key: 'me', name: user.first_name, - symbol: , }) } // Instance @@ -125,7 +115,6 @@ export const breadcrumbsLogic = kea([ breadcrumbs.push({ key: 'instance', name: stripHTTP(preflight.site_url), - symbol: , }) } // Organization @@ -135,14 +124,19 @@ export const breadcrumbsLogic = kea([ } breadcrumbs.push({ key: 'organization', - name: currentOrganization.name, - symbol: , - popover: - otherOrganizations?.length || preflight?.can_create_org - ? { - overlay: , - } - : undefined, + symbol: ( + + + + ), + popover: { + overlay: , + }, }) } // Project diff --git a/frontend/src/layout/navigation/OrganizationSwitcher.tsx b/frontend/src/layout/navigation/OrganizationSwitcher.tsx index d71a52df45c6e..8824bf4f4a2e8 100644 --- a/frontend/src/layout/navigation/OrganizationSwitcher.tsx +++ b/frontend/src/layout/navigation/OrganizationSwitcher.tsx @@ -4,7 +4,7 @@ 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' -import { Lettermark } from 'lib/lemon-ui/Lettermark' +import { UploadedLogo } from 'lib/lemon-ui/UploadedLogo/UploadedLogo' import { membershipLevelToName } from 'lib/utils/permissioning' import { organizationLogic } from 'scenes/organizationLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' @@ -25,7 +25,6 @@ export function AccessLevelIndicator({ organization }: { organization: Organizat export function OtherOrganizationButton({ organization, - index, }: { organization: OrganizationBasicType index: number @@ -35,7 +34,13 @@ export function OtherOrganizationButton({ return ( updateCurrentOrganization(organization.id)} - icon={} + icon={ + + } title={`Switch to organization ${organization.name}`} fullWidth > @@ -83,11 +88,18 @@ export function OrganizationSwitcherOverlay(): JSX.Element { {currentOrganization && ( } + icon={ + + } title={`Switch to organization ${currentOrganization.name}`} + active fullWidth > - {currentOrganization.name} + {currentOrganization.name} )} diff --git a/frontend/src/layout/navigation/TopBar/AccountPopover.tsx b/frontend/src/layout/navigation/TopBar/AccountPopover.tsx index 46c0139e0e91c..0eeada32885d2 100644 --- a/frontend/src/layout/navigation/TopBar/AccountPopover.tsx +++ b/frontend/src/layout/navigation/TopBar/AccountPopover.tsx @@ -16,9 +16,9 @@ import { LemonButtonPropsBase } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { Lettermark } from 'lib/lemon-ui/Lettermark' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { UploadedLogo } from 'lib/lemon-ui/UploadedLogo' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { ThemeSwitcher } from 'scenes/settings/user/ThemeSwitcher' @@ -88,7 +88,13 @@ function CurrentOrganization({ organization }: { organization: OrganizationBasic } + icon={ + + } sideIcon={} fullWidth to={urls.settings('organization')} diff --git a/frontend/src/lib/api.mock.ts b/frontend/src/lib/api.mock.ts index 4b9bc79edf43a..c6ca0a28c2cda 100644 --- a/frontend/src/lib/api.mock.ts +++ b/frontend/src/lib/api.mock.ts @@ -105,6 +105,7 @@ export const MOCK_DEFAULT_ORGANIZATION: OrganizationType = { metadata: {}, available_product_features: [], member_count: 2, + logo_media_id: null, } export const MOCK_DEFAULT_BASIC_USER: UserBasicType = { @@ -139,6 +140,7 @@ export const MOCK_DEFAULT_USER: UserType = { name, slug, membership_level, + logo_media_id: null, })), events_column_config: { active: 'DEFAULT', diff --git a/frontend/src/lib/components/Alerts/AlertDeletionWarning.tsx b/frontend/src/lib/components/Alerts/AlertDeletionWarning.tsx index 032f217427320..3fb22eeff5c53 100644 --- a/frontend/src/lib/components/Alerts/AlertDeletionWarning.tsx +++ b/frontend/src/lib/components/Alerts/AlertDeletionWarning.tsx @@ -6,15 +6,12 @@ import { alertsLogic } from './alertsLogic' export function AlertDeletionWarning(): JSX.Element | null { const { insightProps, queryBasedInsight: insight } = useValues(insightLogic) - if (!insight.short_id) { - return null - } const { shouldShowAlertDeletionWarning } = useValues( - alertsLogic({ insightShortId: insight.short_id, insightLogicProps: insightProps }) + alertsLogic({ insightShortId: insight.short_id ?? '', insightLogicProps: insightProps }) ) - if (!shouldShowAlertDeletionWarning) { + if (!shouldShowAlertDeletionWarning || !insight.short_id) { return null } diff --git a/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx b/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx index 16507a4890be5..7d51c49598657 100644 --- a/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx +++ b/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx @@ -8,6 +8,8 @@ import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { LemonTable } from 'lib/lemon-ui/LemonTable' +import { LemonTag } from 'lib/lemon-ui/LemonTag' +import { Link } from 'lib/lemon-ui/Link' import { humanizeBytes } from 'lib/utils' import { copyToClipboard } from 'lib/utils/copyToClipboard' import { useState } from 'react' @@ -30,7 +32,6 @@ export interface Query { timestamp: string query: string query_id: string - queryJson: string exception: string /** * 1 means running, 2 means finished, 3 means errored before execution, 4 means errored during execution. @@ -39,6 +40,10 @@ export interface Query { status: 1 | 2 | 3 | 4 execution_time: number path: string + logComment: { + query: any + [key: string]: any + } } const debugCHQueriesLogic = kea([ @@ -59,7 +64,7 @@ const debugCHQueriesLogic = kea([ [] as Query[], { loadQueries: async () => { - return await api.get('api/debug_ch_queries/') + return (await api.get('api/debug_ch_queries/')).queries }, }, ], @@ -128,9 +133,20 @@ function DebugCHQueries(): JSX.Element { title: 'Timestamp', render: function Timestamp(_, item) { return ( - - {dayjs.tz(item.timestamp, 'UTC').tz().format().replace('T', '\n')} - + <> +
+ {dayjs.tz(item.timestamp, 'UTC').tz().format().replace('T', '\n')} +
+
+ {item.status === 1 ? ( + 'In progress…' + ) : ( + <> + Took {Math.round((item.execution_time + Number.EPSILON) * 100) / 100} ms + + )} +
+ ) }, width: 160, @@ -141,12 +157,71 @@ function DebugCHQueries(): JSX.Element { return (
- ID:{' '} - {item.query_id} + + ID:{' '} + {item.query_id} + {' '} + {item.logComment.cache_key ? ( + + Cache key:{' '} + {item.logComment.cache_key}{' '} + + + ) : null}{' '} + {item.logComment.insight_id ? ( + + Insight ID:{' '} + {item.logComment.insight_id}{' '} + + + ) : null}{' '} + {item.logComment.dashboard_id ? ( + + Dashboard ID:{' '} + {item.logComment.dashboard_id}{' '} + + + ) : null}{' '} + {item.logComment.user_id ? ( + + User ID:{' '} + {item.logComment.user_id}{' '} + + + ) : null}
{item.exception && ( - {item.exception} +
{item.exception}
+ + View in Sentry +
)} {item.query} - {item.queryJson ? ( + {item.logComment.query ? ( } - to={urls.debugQuery(item.queryJson)} + to={urls.debugQuery(item.logComment.query)} targetBlank sideAction={{ icon: , - onClick: () => void copyToClipboard(item.queryJson, 'query JSON'), + onClick: () => + void copyToClipboard( + JSON.stringify(item.logComment.query), + 'query JSON' + ), tooltip: 'Copy query JSON to clipboard', }} className="my-0" > - Debug {JSON.parse(item.queryJson).kind || 'query'} in new tab + Debug {item.logComment.query.kind || 'query'} in new tab ) : null}
) }, }, - - { - title: 'Duration', - render: function Duration(_, item) { - if (item.status === 1) { - return 'In progress…' - } - return <>{Math.round((item.execution_time + Number.EPSILON) * 100) / 100} ms - }, - align: 'right', - }, { title: 'Profiling stats', render: function ProfilingStats(_, item) { @@ -287,6 +355,7 @@ function DebugCHQueries(): JSX.Element { loading={queriesLoading} loadingSkeletonRows={5} pagination={undefined} + rowClassName="align-top" /> ) diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx b/frontend/src/lib/components/FeedbackNotice.tsx similarity index 76% rename from frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx rename to frontend/src/lib/components/FeedbackNotice.tsx index ef0f2e268c822..78de290d2ae3f 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx +++ b/frontend/src/lib/components/FeedbackNotice.tsx @@ -1,12 +1,13 @@ import { IconBug } from '@posthog/icons' -import { LemonButton } from '@posthog/lemon-ui' +import { LemonBanner } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { supportLogic } from 'lib/components/Support/supportLogic' import { IconFeedback } from 'lib/lemon-ui/icons' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { LemonButton } from 'lib/lemon-ui/LemonButton' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -export const WebAnalyticsNotice = (): JSX.Element => { +import { supportLogic } from './Support/supportLogic' + +export const FeedbackNotice = ({ text }: { text: string }): JSX.Element => { const { openSupportForm } = useActions(supportLogic) const { preflight } = useValues(preflightLogic) @@ -15,9 +16,7 @@ export const WebAnalyticsNotice = (): JSX.Element => { return (
-
- PostHog Web Analytics is in open beta. Thanks for taking part! We'd love to hear what you think. -
+
{text}
{showSupportOptions ? ( void + onUpload?: (url: string, fileName: string, uploadedMediaId: string) => void onError: (detail: string) => void }): { setFilesToUpload: (files: File[]) => void @@ -58,7 +58,7 @@ export function useUploadFiles({ setUploading(true) const file: File = filesToUpload[0] const media = await uploadFile(file) - onUpload?.(media.image_location, media.name) + onUpload?.(media.image_location, media.name, media.id) } catch (error) { const errorDetail = (error as any).detail || 'unknown error' onError(errorDetail) diff --git a/frontend/src/lib/lemon-ui/LemonField/LemonField.tsx b/frontend/src/lib/lemon-ui/LemonField/LemonField.tsx index db9d9b65218c7..77e3dce5c3132 100644 --- a/frontend/src/lib/lemon-ui/LemonField/LemonField.tsx +++ b/frontend/src/lib/lemon-ui/LemonField/LemonField.tsx @@ -43,7 +43,7 @@ const LemonPureField = ({ onClick={onClick} className={clsx( 'Field flex', - { 'gap-2': className && className.indexOf('gap-') === -1 }, + { 'gap-2': className ? className.indexOf('gap-') === -1 : true }, className, error && 'Field--error', inline ? 'flex-row' : 'flex-col' diff --git a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx index 294b85a177440..4aa9366f6ddea 100644 --- a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx +++ b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx @@ -136,7 +136,10 @@ export const LemonFileInput = ({ <>
- + {({ value, onChange }) => ( <> acc + x.rating, 0) / hogFunction.status.ratings.length - : 0 - return ( {display} -

- Your function has{' '} - {noRatings ? ( - <> - no ratings yet. There are either no recent invocations or data is still being - gathered. - - ) : ( - <> - a rating of {Math.round(averageRating * 100)}%. - - )}{' '} - A rating of 100% means the function is running perfectly, with 0% meaning it is failing - every time. -

-

{description}

- -

History

-
    - , - }, - { - title: 'Status', - key: 'state', - render: (_, { state }) => { - const { tagType, display } = displayMap[state] || DEFAULT_DISPLAY - return {display} - }, - }, - ]} - dataSource={hogFunction.status?.states ?? []} - /> -
} diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts index 83105f0c7f8ee..644fa16ab97c0 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts @@ -293,6 +293,9 @@ export const sessionRecordingsPlaylistLogic = kea ({ diff --git a/frontend/src/scenes/settings/Settings.tsx b/frontend/src/scenes/settings/Settings.tsx index 768cd594c7549..a5bb47abf3ea6 100644 --- a/frontend/src/scenes/settings/Settings.tsx +++ b/frontend/src/scenes/settings/Settings.tsx @@ -123,7 +123,7 @@ function SettingsRenderer(props: SettingsLogicProps): JSX.Element { }} />

- {x.title}{' '} + {x.title} } size="small" onClick={() => selectSetting?.(x.id)} />

{x.description &&

{x.description}

} diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index f4bbb968a3e6c..a96470fd792c4 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -8,6 +8,7 @@ import { Members } from './organization/Members' import { OrganizationDangerZone } from './organization/OrganizationDangerZone' import { OrganizationDisplayName } from './organization/OrgDisplayName' import { OrganizationEmailPreferences } from './organization/OrgEmailPreferences' +import { OrganizationLogo } from './organization/OrgLogo' import { PermissionsGrid } from './organization/Permissions/PermissionsGrid' import { VerifiedDomains } from './organization/VerifiedDomains/VerifiedDomains' import { @@ -309,6 +310,11 @@ export const SettingsMap: SettingSection[] = [ title: 'Display name', component: , }, + { + id: 'organization-logo', + title: 'Logo', + component: , + }, ], }, { diff --git a/frontend/src/scenes/settings/organization/OrgDisplayName.tsx b/frontend/src/scenes/settings/organization/OrgDisplayName.tsx index 895a27fe305d0..b48616bfa9677 100644 --- a/frontend/src/scenes/settings/organization/OrgDisplayName.tsx +++ b/frontend/src/scenes/settings/organization/OrgDisplayName.tsx @@ -31,7 +31,7 @@ export function OrganizationDisplayName(): JSX.Element { disabled={isRestricted || !name || !currentOrganization || name === currentOrganization.name} loading={currentOrganizationLoading} > - Rename Organization + Rename organization
) diff --git a/frontend/src/scenes/settings/organization/OrgLogo.tsx b/frontend/src/scenes/settings/organization/OrgLogo.tsx new file mode 100644 index 0000000000000..9fa44b1e823cb --- /dev/null +++ b/frontend/src/scenes/settings/organization/OrgLogo.tsx @@ -0,0 +1,90 @@ +import { IconX } from '@posthog/icons' +import { LemonButton, LemonFileInput, lemonToast } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { useRestrictedArea } from 'lib/components/RestrictedArea' +import { OrganizationMembershipLevel } from 'lib/constants' +import { useUploadFiles } from 'lib/hooks/useUploadFiles' +import { IconUploadFile } from 'lib/lemon-ui/icons' +import { UploadedLogo } from 'lib/lemon-ui/UploadedLogo/UploadedLogo' +import { useState } from 'react' +import { organizationLogic } from 'scenes/organizationLogic' + +export function OrganizationLogo(): JSX.Element { + const { currentOrganization, currentOrganizationLoading } = useValues(organizationLogic) + const { updateOrganization } = useActions(organizationLogic) + + const [logoMediaId, setLogoMediaId] = useState(currentOrganization?.logo_media_id || null) + + const { setFilesToUpload, filesToUpload, uploading } = useUploadFiles({ + onUpload: (_, __, id) => { + setLogoMediaId(id) + }, + onError: (detail) => { + lemonToast.error(`Error uploading image: ${detail}`) + }, + }) + + const isRestricted = !!useRestrictedArea({ minimumAccessLevel: OrganizationMembershipLevel.Admin }) + + return ( +
+ +
+ + {logoMediaId && ( +
+ } + onClick={(e) => { + setLogoMediaId(null) + e.preventDefault() // Don't fire LemonFileInput's handler + }} + size="small" + tooltip="Reset back to lettermark" + tooltipPlacement="right" + noPadding + className="group-hover:flex hidden absolute right-0 top-0" + /> +
+ )} +
+ +
+ Click or drag and drop to upload logo image +
+ (192x192 px or larger) +
+ + } + /> + { + e.preventDefault() + updateOrganization({ logo_media_id: logoMediaId }) + }} + disabled={ + isRestricted || + !currentOrganization || + logoMediaId == currentOrganization.logo_media_id || + uploading + } + loading={currentOrganizationLoading} + > + Save logo + +
+ ) +} diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap b/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap index 4ada7b6b11b44..f97f2175d40aa 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap @@ -20,6 +20,7 @@ exports[`verifiedDomainsLogic values has proper defaults 1`] = ` "enforce_2fa": false, "id": "ABCD", "is_member_join_email_enabled": true, + "logo_media_id": null, "member_count": 2, "membership_level": 8, "metadata": {}, diff --git a/frontend/src/scenes/settings/project/ProjectSettings.tsx b/frontend/src/scenes/settings/project/ProjectSettings.tsx index a46ae1422f2d8..1f3a634c59948 100644 --- a/frontend/src/scenes/settings/project/ProjectSettings.tsx +++ b/frontend/src/scenes/settings/project/ProjectSettings.tsx @@ -40,7 +40,7 @@ export function ProjectDisplayName(): JSX.Element { disabled={!name || !currentTeam || name === currentTeam.name} loading={currentTeamLoading} > - Rename Project + Rename project
) diff --git a/frontend/src/scenes/settings/types.ts b/frontend/src/scenes/settings/types.ts index bde791c278a09..86143478a6f83 100644 --- a/frontend/src/scenes/settings/types.ts +++ b/frontend/src/scenes/settings/types.ts @@ -63,6 +63,7 @@ export type SettingId = | 'integration-ip-allowlist' | 'project-rbac' | 'project-delete' + | 'organization-logo' | 'organization-display-name' | 'invites' | 'members' diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsRecordings.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsRecordings.tsx new file mode 100644 index 0000000000000..6ebaf0bcaea46 --- /dev/null +++ b/frontend/src/scenes/web-analytics/WebAnalyticsRecordings.tsx @@ -0,0 +1,83 @@ +import { IconRewindPlay } from '@posthog/icons' +import clsx from 'clsx' +import { useValues } from 'kea' +import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage' +import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { RecordingRow } from 'scenes/project-homepage/RecentRecordings' +import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' +import { sessionRecordingsPlaylistLogic } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic' +import { teamLogic } from 'scenes/teamLogic' +import { urls } from 'scenes/urls' +import { ReplayTile, webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' + +import { ReplayTabs } from '~/types' + +export function WebAnalyticsRecordingsTile({ tile }: { tile: ReplayTile }): JSX.Element { + const { layout } = tile + const { replayFilters, webAnalyticsFilters } = useValues(webAnalyticsLogic) + const { currentTeam } = useValues(teamLogic) + const sessionRecordingsListLogicInstance = sessionRecordingsPlaylistLogic({ + logicKey: 'webAnalytics', + filters: replayFilters, + }) + + const { sessionRecordings, sessionRecordingsResponseLoading } = useValues(sessionRecordingsListLogicInstance) + const items = sessionRecordings.slice(0, 5) + + const emptyMessage = !currentTeam?.session_recording_opt_in + ? { + title: 'Recordings are not enabled for this project', + description: 'Once recordings are enabled, new recordings will display here.', + buttonText: 'Enable recordings', + buttonTo: urls.settings('project-replay'), + } + : webAnalyticsFilters.length > 0 + ? { + title: 'There are no recordings matching the current filters', + description: 'Try changing the filters, or view all recordings.', + buttonText: 'View all', + buttonTo: urls.replay(), + } + : { + title: 'There are no recordings matching this date range', + description: 'Make sure you have the javascript snippet setup in your website.', + buttonText: 'Learn more', + buttonTo: 'https://posthog.com/docs/user-guides/recordings', + } + const to = items.length > 0 ? urls.replay(ReplayTabs.Recent, replayFilters) : urls.replay() + return ( + <> + +
+

Session replay

+
+ {sessionRecordingsResponseLoading ? ( +
+ {Array.from({ length: 6 }, (_, index) => ( + + ))} +
+ ) : items.length === 0 && emptyMessage ? ( + + ) : ( + items.map((item, index) => ) + )} +
+
+ } size="small" type="secondary"> + View all + +
+
+ + ) +} diff --git a/frontend/src/scenes/web-analytics/WebDashboard.tsx b/frontend/src/scenes/web-analytics/WebDashboard.tsx index 79b62a6c739f4..23a5ea403050f 100644 --- a/frontend/src/scenes/web-analytics/WebDashboard.tsx +++ b/frontend/src/scenes/web-analytics/WebDashboard.tsx @@ -2,6 +2,7 @@ import { IconExpand45, IconInfo, IconOpenSidebar, IconX } from '@posthog/icons' import clsx from 'clsx' import { BindLogic, useActions, useValues } from 'kea' import { DateFilter } from 'lib/components/DateFilter/DateFilter' +import { FeedbackNotice } from 'lib/components/FeedbackNotice' import { VersionCheckerBanner } from 'lib/components/VersionChecker/VersionCheckerBanner' import { IconOpenInNew } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' @@ -20,7 +21,7 @@ import { webAnalyticsLogic, } from 'scenes/web-analytics/webAnalyticsLogic' import { WebAnalyticsModal } from 'scenes/web-analytics/WebAnalyticsModal' -import { WebAnalyticsNotice } from 'scenes/web-analytics/WebAnalyticsNotice' +import { WebAnalyticsRecordingsTile } from 'scenes/web-analytics/WebAnalyticsRecordings' import { WebQuery } from 'scenes/web-analytics/WebAnalyticsTile' import { WebPropertyFilters } from 'scenes/web-analytics/WebPropertyFilters' @@ -65,10 +66,12 @@ const Tiles = (): JSX.Element => { return (
{tiles.map((tile, i) => { - if ('query' in tile) { + if (tile.kind === 'query') { return - } else if ('tabs' in tile) { + } else if (tile.kind === 'tabs') { return + } else if (tile.kind === 'replay') { + return } return null })} @@ -201,16 +204,17 @@ export const WebTabs = ({ }[] setActiveTabId: (id: string) => void openModal: (tileId: TileId, tabId: string) => void - getNewInsightUrl: (tileId: TileId, tabId: string) => string + getNewInsightUrl: (tileId: TileId, tabId: string) => string | undefined tileId: TileId }): JSX.Element => { const activeTab = tabs.find((t) => t.id === activeTabId) + const newInsightUrl = getNewInsightUrl(tileId, activeTabId) const buttonsRow = [ - activeTab?.canOpenInsight ? ( + activeTab?.canOpenInsight && newInsightUrl ? ( } size="small" type="secondary" @@ -319,7 +323,7 @@ export const WebAnalyticsDashboard = (): JSX.Element => { - +
diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx b/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx index 16970fccf62be..e1e9eaa03da50 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx @@ -3,7 +3,7 @@ import { loaders } from 'kea-loaders' import { actionToUrl, urlToAction } from 'kea-router' import { windowValues } from 'kea-window-values' import api from 'lib/api' -import { RETENTION_FIRST_TIME, STALE_EVENT_SECONDS } from 'lib/constants' +import { FEATURE_FLAGS, RETENTION_FIRST_TIME, STALE_EVENT_SECONDS } from 'lib/constants' import { dayjs } from 'lib/dayjs' import { Link, PostHogComDocsURL } from 'lib/lemon-ui/Link/Link' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' @@ -23,6 +23,7 @@ import { ChartDisplayType, EventDefinition, EventDefinitionType, + FilterLogicalOperator, InsightLogicProps, InsightType, IntervalType, @@ -31,6 +32,7 @@ import { PropertyDefinition, PropertyFilterType, PropertyOperator, + RecordingUniversalFilters, RetentionPeriod, } from '~/types' @@ -54,6 +56,7 @@ export enum TileId { DEVICES = 'DEVICES', GEOGRAPHY = 'GEOGRAPHY', RETENTION = 'RETENTION', + REPLAY = 'REPLAY', } const loadPriorityMap: Record = { @@ -64,6 +67,7 @@ const loadPriorityMap: Record = { [TileId.DEVICES]: 5, [TileId.GEOGRAPHY]: 6, [TileId.RETENTION]: 7, + [TileId.REPLAY]: 8, } interface BaseTile { @@ -77,6 +81,7 @@ export interface Docs { description: string | JSX.Element } export interface QueryTile extends BaseTile { + kind: 'query' title?: string query: QuerySchema showIntervalSelect?: boolean @@ -101,12 +106,17 @@ export interface TabsTileTab { } export interface TabsTile extends BaseTile { + kind: 'tabs' activeTabId: string setTabId: (id: string) => void tabs: TabsTileTab[] } -export type WebDashboardTile = QueryTile | TabsTile +export interface ReplayTile extends BaseTile { + kind: 'replay' +} + +export type WebDashboardTile = QueryTile | TabsTile | ReplayTile export interface WebDashboardModalQuery { tileId: TileId @@ -438,6 +448,7 @@ export const webAnalyticsLogic = kea([ () => values.statusCheck, () => values.isGreaterThanMd, () => values.shouldShowGeographyTile, + () => values.featureFlags, ], ( webAnalyticsFilters, @@ -447,7 +458,8 @@ export const webAnalyticsLogic = kea([ filterTestAccounts, _statusCheck, isGreaterThanMd, - shouldShowGeographyTile + shouldShowGeographyTile, + featureFlags ): WebDashboardTile[] => { const dateRange = { date_from: dateFrom, @@ -469,6 +481,7 @@ export const webAnalyticsLogic = kea([ const allTiles: (WebDashboardTile | null)[] = [ { + kind: 'query', tileId: TileId.OVERVIEW, layout: { colSpanClassName: 'md:col-span-full', @@ -486,6 +499,7 @@ export const webAnalyticsLogic = kea([ canOpenModal: false, }, { + kind: 'tabs', tileId: TileId.GRAPHS, layout: { colSpanClassName: `md:col-span-2`, @@ -603,6 +617,7 @@ export const webAnalyticsLogic = kea([ ], }, { + kind: 'tabs', tileId: TileId.PATHS, layout: { colSpanClassName: `md:col-span-2`, @@ -689,6 +704,7 @@ export const webAnalyticsLogic = kea([ ).filter(isNotNil), }, { + kind: 'tabs', tileId: TileId.SOURCES, layout: { colSpanClassName: `md:col-span-1`, @@ -879,6 +895,7 @@ export const webAnalyticsLogic = kea([ ], }, { + kind: 'tabs', tileId: TileId.DEVICES, layout: { colSpanClassName: `md:col-span-1`, @@ -966,9 +983,9 @@ export const webAnalyticsLogic = kea([ }, ], }, - shouldShowGeographyTile ? { + kind: 'tabs', tileId: TileId.GEOGRAPHY, layout: { colSpanClassName: 'md:col-span-full', @@ -1073,6 +1090,7 @@ export const webAnalyticsLogic = kea([ } : null, { + kind: 'query', tileId: TileId.RETENTION, title: 'Retention', layout: { @@ -1105,6 +1123,15 @@ export const webAnalyticsLogic = kea([ canOpenInsight: true, canOpenModal: false, }, + featureFlags[FEATURE_FLAGS.WEB_ANALYTICS_REPLAY] + ? { + kind: 'replay', + tileId: TileId.REPLAY, + layout: { + colSpanClassName: 'md:col-span-1', + }, + } + : null, ] return allTiles.filter(isNotNil) }, @@ -1134,10 +1161,7 @@ export const webAnalyticsLogic = kea([ return query } - if (tabId) { - if (!('tabs' in tile)) { - throw new Error('Developer Error, tabId provided for non-tab tile') - } + if (tile.kind === 'tabs') { const tab = tile.tabs.find((tab) => tab.id === tabId) if (!tab) { throw new Error('Developer Error, tab not found') @@ -1157,22 +1181,21 @@ export const webAnalyticsLogic = kea([ query: extendQuery(tab.query), canOpenInsight: tab.canOpenInsight, } + } else if (tile.kind === 'query') { + return { + tileId, + title: tile.title, + showIntervalSelect: tile.showIntervalSelect, + showPathCleaningControls: tile.showPathCleaningControls, + insightProps: { + dashboardItemId: getDashboardItemId(tileId, undefined, true), + loadPriority: 0, + dataNodeCollectionId: WEB_ANALYTICS_DATA_COLLECTION_NODE_ID, + }, + query: extendQuery(tile.query), + } } - if ('tabs' in tile) { - throw new Error('Developer Error, tabId not provided for tab tile') - } - return { - tileId, - title: tile.title, - showIntervalSelect: tile.showIntervalSelect, - showPathCleaningControls: tile.showPathCleaningControls, - insightProps: { - dashboardItemId: getDashboardItemId(tileId, undefined, true), - loadPriority: 0, - dataNodeCollectionId: WEB_ANALYTICS_DATA_COLLECTION_NODE_ID, - }, - query: extendQuery(tile.query), - } + return null }, ], hasCountryFilter: [ @@ -1199,10 +1222,42 @@ export const webAnalyticsLogic = kea([ return webAnalyticsFilters.some((filter) => filter.key === '$os') }, ], + replayFilters: [ + (s) => [s.webAnalyticsFilters, s.dateFilter, s.shouldFilterTestAccounts], + ( + webAnalyticsFilters: WebAnalyticsPropertyFilters, + dateFilter, + shouldFilterTestAccounts + ): RecordingUniversalFilters => { + return { + filter_test_accounts: shouldFilterTestAccounts, + + date_from: dateFilter.dateFrom, + date_to: dateFilter.dateTo, + filter_group: { + type: FilterLogicalOperator.And, + values: [ + { + type: FilterLogicalOperator.And, + values: webAnalyticsFilters || [], + }, + ], + }, + duration: [ + { + type: PropertyFilterType.Recording, + key: 'active_seconds', + operator: PropertyOperator.GreaterThan, + value: 1, + }, + ], + } + }, + ], getNewInsightUrl: [ (s) => [s.webAnalyticsFilters, s.dateFilter, s.tiles], (webAnalyticsFilters: WebAnalyticsPropertyFilters, { dateTo, dateFrom }, tiles) => { - return function getNewInsightUrl(tileId: TileId, tabId?: string): string { + return function getNewInsightUrl(tileId: TileId, tabId?: string): string | undefined { const formatQueryForNewInsight = (query: QuerySchema): QuerySchema => { if (query.kind === NodeKind.InsightVizNode) { return { @@ -1218,10 +1273,7 @@ export const webAnalyticsLogic = kea([ if (!tile) { throw new Error('Developer Error, tile not found') } - if (tabId) { - if (!('tabs' in tile)) { - throw new Error('Developer Error, tabId provided for non-tab tile') - } + if (tile.kind === 'tabs') { const tab = tile.tabs.find((tab) => tab.id === tabId) if (!tab) { throw new Error('Developer Error, tab not found') @@ -1231,15 +1283,15 @@ export const webAnalyticsLogic = kea([ null, formatQueryForNewInsight(tab.query) ) + } else if (tile.kind === 'query') { + return urls.insightNew( + { properties: webAnalyticsFilters, date_from: dateFrom, date_to: dateTo }, + null, + formatQueryForNewInsight(tile.query) + ) + } else if (tile.kind === 'replay') { + return urls.replay() } - if ('tabs' in tile) { - throw new Error('Developer Error, tabId not provided for tab tile') - } - return urls.insightNew( - { properties: webAnalyticsFilters, date_from: dateFrom, date_to: dateTo }, - null, - formatQueryForNewInsight(tile.query) - ) } }, ], diff --git a/frontend/src/styles/global.scss b/frontend/src/styles/global.scss index 54dd8dfda4bb7..5cf14619c4479 100644 --- a/frontend/src/styles/global.scss +++ b/frontend/src/styles/global.scss @@ -37,7 +37,9 @@ ul { } // This may look odd but sets up our utility classes -* { +*, +*::before, +*::after { border-color: var(--border); border-style: solid; border-width: 0; @@ -340,6 +342,7 @@ input::-ms-clear { // AntD overrrides, placed inside `body` to increase specifity (nicely avoiding the need for !important) body { // Remove below once we're using Tailwind's base + --tw-content: ''; --tw-ring-offset-width: 0px; --tw-ring-offset-color: var(--bg-light); --tw-ring-color: var(--primary-3000); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 5fff2acf62b6e..94c3d0a3e3c75 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -333,6 +333,7 @@ export interface OrganizationBasicType { id: string name: string slug: string + logo_media_id: string | null membership_level: OrganizationMembershipLevel | null } @@ -3316,26 +3317,34 @@ export interface DateMappingOption { interface BreadcrumbBase { /** E.g. scene, tab, or scene with item ID. Particularly important for `onRename`. */ key: string | number | [scene: Scene, key: string | number] - /** Name to display. */ - name: string | null | undefined - /** Symbol, e.g. a lettermark or a profile picture. */ - symbol?: React.ReactNode /** Whether to show a custom popover */ popover?: Pick } interface LinkBreadcrumb extends BreadcrumbBase { + /** Name to display. */ + name: string | null | undefined + symbol?: never /** Path to link to. */ path?: string onRename?: never } interface RenamableBreadcrumb extends BreadcrumbBase { + /** Name to display. */ + name: string | null | undefined + symbol?: never path?: never /** When this is set, an "Edit" button shows up next to the title */ onRename?: (newName: string) => Promise /** When this is true, the name is always in edit mode, and `onRename` runs on every input change. */ forceEditMode?: boolean } -export type Breadcrumb = LinkBreadcrumb | RenamableBreadcrumb +interface SymbolBreadcrumb extends BreadcrumbBase { + name?: never + /** Symbol, e.g. a lettermark or a profile picture. */ + symbol: React.ReactElement + path?: never +} +export type Breadcrumb = LinkBreadcrumb | RenamableBreadcrumb | SymbolBreadcrumb export enum GraphType { Bar = 'bar', @@ -4331,14 +4340,8 @@ export enum HogWatcherState { export type HogFunctionStatus = { state: HogWatcherState - states: { - timestamp: number - state: HogWatcherState - }[] - ratings: { - timestamp: number - rating: number - }[] + rating: number + tokens: number } export type HogFunctionInvocationGlobals = { diff --git a/latest_migrations.manifest b/latest_migrations.manifest index 3079c62e846e3..effcacbffe9aa 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name ee: 0016_rolemembership_organization_member otp_static: 0002_throttling otp_totp: 0002_auto_20190420_0723 -posthog: 0451_datawarehousetable_updated_at_and_more +posthog: 0452_organization_logo sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/package.json b/package.json index 4d023d4d6736e..f257d70a5da80 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "husky": "^7.0.4", "image-blob-reduce": "^4.1.0", "kea": "^3.1.5", - "kea-forms": "^3.1.3", + "kea-forms": "^3.2.0", "kea-loaders": "^3.0.0", "kea-localstorage": "^3.1.0", "kea-router": "^3.1.4", @@ -150,7 +150,7 @@ "pmtiles": "^2.11.0", "postcss": "^8.4.31", "postcss-preset-env": "^9.3.0", - "posthog-js": "1.154.5", + "posthog-js": "1.154.6", "posthog-js-lite": "3.0.0", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/plugin-server/src/cdp/cdp-api.ts b/plugin-server/src/cdp/cdp-api.ts index 4d9cd53c58cc5..553e380e16cdf 100644 --- a/plugin-server/src/cdp/cdp-api.ts +++ b/plugin-server/src/cdp/cdp-api.ts @@ -8,8 +8,7 @@ import { delay } from '../utils/utils' import { AsyncFunctionExecutor } from './async-function-executor' import { HogExecutor } from './hog-executor' import { HogFunctionManager } from './hog-function-manager' -import { HogWatcher } from './hog-watcher/hog-watcher' -import { HogWatcherState } from './hog-watcher/types' +import { HogWatcher, HogWatcherState } from './hog-watcher' import { HogFunctionInvocation, HogFunctionInvocationAsyncRequest, HogFunctionType, LogEntry } from './types' export class CdpApi { @@ -52,7 +51,7 @@ export class CdpApi { () => async (req: express.Request, res: express.Response): Promise => { const { id } = req.params - const summary = await this.hogWatcher.fetchWatcher(id) + const summary = await this.hogWatcher.getState(id) res.json(summary) } @@ -69,7 +68,7 @@ export class CdpApi { return } - const summary = await this.hogWatcher.fetchWatcher(id) + const summary = await this.hogWatcher.getState(id) // Only allow patching the status if it is different from the current status @@ -80,7 +79,7 @@ export class CdpApi { // Hacky - wait for a little to give a chance for the state to change await delay(100) - res.json(await this.hogWatcher.fetchWatcher(id)) + res.json(await this.hogWatcher.getState(id)) } private postFunctionInvocation = async (req: express.Request, res: express.Response): Promise => { diff --git a/plugin-server/src/cdp/cdp-consumers.ts b/plugin-server/src/cdp/cdp-consumers.ts index ca0accfc157dc..2fa9ae1eda4a4 100644 --- a/plugin-server/src/cdp/cdp-consumers.ts +++ b/plugin-server/src/cdp/cdp-consumers.ts @@ -17,6 +17,7 @@ import { addSentryBreadcrumbsEventListeners } from '../main/ingestion-queues/kaf import { runInstrumentedFunction } from '../main/utils' import { AppMetric2Type, Hub, RawClickHouseEvent, TeamId, TimestampFormat } from '../types' import { KafkaProducerWrapper } from '../utils/db/kafka-producer-wrapper' +import { captureTeamEvent } from '../utils/posthog' import { status } from '../utils/status' import { castTimestampOrNow } from '../utils/utils' import { RustyHook } from '../worker/rusty-hook' @@ -24,8 +25,7 @@ import { AsyncFunctionExecutor } from './async-function-executor' import { GroupsManager } from './groups-manager' import { HogExecutor } from './hog-executor' import { HogFunctionManager } from './hog-function-manager' -import { HogWatcher } from './hog-watcher/hog-watcher' -import { HogWatcherState } from './hog-watcher/types' +import { HogWatcher, HogWatcherState } from './hog-watcher' import { CdpOverflowMessage, HogFunctionAsyncFunctionResponse, @@ -96,14 +96,38 @@ abstract class CdpConsumerBase { protected heartbeat = () => {} constructor(protected hub: Hub) { - this.hogWatcher = new HogWatcher(hub) this.hogFunctionManager = new HogFunctionManager(hub.postgres, hub) + this.hogWatcher = new HogWatcher(hub, (id, state) => { + void this.captureInternalPostHogEvent(id, 'hog function state changed', { state }) + }) this.hogExecutor = new HogExecutor(this.hogFunctionManager) const rustyHook = this.hub?.rustyHook ?? new RustyHook(this.hub) this.asyncFunctionExecutor = new AsyncFunctionExecutor(this.hub, rustyHook) this.groupsManager = new GroupsManager(this.hub) } + private async captureInternalPostHogEvent( + hogFunctionId: HogFunctionType['id'], + event: string, + properties: any = {} + ) { + const hogFunction = this.hogFunctionManager.getHogFunction(hogFunctionId) + if (!hogFunction) { + return + } + const team = await this.hub.teamManager.fetchTeam(hogFunction.team_id) + + if (!team) { + return + } + + captureTeamEvent(team, event, { + ...properties, + hog_function_id: hogFunctionId, + hog_function_url: `${this.hub.SITE_URL}/project/${team.id}/pipeline/destinations/hog-${hogFunctionId}`, + }) + } + protected async runWithHeartbeat(func: () => Promise | T): Promise { // Helper function to ensure that looping over lots of hog functions doesn't block up the thread, killing the consumer const res = await func() @@ -257,8 +281,6 @@ abstract class CdpConsumerBase { return await runInstrumentedFunction({ statsKey: `cdpConsumer.handleEachBatch.executeAsyncResponses`, func: async () => { - // NOTE: Disabled for now as it needs some rethinking - // this.hogWatcher.currentObservations.observeAsyncFunctionResponses(asyncResponses) asyncResponses.forEach((x) => { counterAsyncFunctionResponse.inc({ outcome: x.asyncFunctionResponse.error ? 'failed' : 'succeeded', @@ -286,7 +308,7 @@ abstract class CdpConsumerBase { this.hogExecutor.executeAsyncResponse(...item) ) - this.hogWatcher.currentObservations.observeResults(results) + await this.hogWatcher.observeResults(results) return results }, }) @@ -298,14 +320,23 @@ abstract class CdpConsumerBase { return await runInstrumentedFunction({ statsKey: `cdpConsumer.handleEachBatch.executeMatchingFunctions`, func: async () => { - const invocations: { globals: HogFunctionInvocationGlobals; hogFunction: HogFunctionType }[] = [] + const possibleInvocations: { globals: HogFunctionInvocationGlobals; hogFunction: HogFunctionType }[] = + [] // TODO: Add a helper to hog functions to determine if they require groups or not and then only load those await this.groupsManager.enrichGroups(invocationGlobals) + // Find all functions that could need running invocationGlobals.forEach((globals) => { const { matchingFunctions, nonMatchingFunctions } = this.hogExecutor.findMatchingFunctions(globals) + possibleInvocations.push( + ...matchingFunctions.map((hogFunction) => ({ + globals, + hogFunction, + })) + ) + nonMatchingFunctions.forEach((item) => this.produceAppMetric({ team_id: item.team_id, @@ -315,59 +346,52 @@ abstract class CdpConsumerBase { count: 1, }) ) + }) - // Filter for overflowed and disabled functions - const hogFunctionsByState = matchingFunctions.reduce((acc, item) => { - const state = this.hogWatcher.getFunctionState(item.id) - return { - ...acc, - [state]: [...(acc[state] ?? []), item], - } - }, {} as Record) - - if (hogFunctionsByState[HogWatcherState.overflowed]?.length) { - const overflowed = hogFunctionsByState[HogWatcherState.overflowed]! - // Group all overflowed functions into one event - counterFunctionInvocation.inc({ outcome: 'overflowed' }, overflowed.length) - - this.messagesToProduce.push({ - topic: KAFKA_CDP_FUNCTION_OVERFLOW, - value: { - source: 'event_invocations', - payload: { - hogFunctionIds: overflowed.map((x) => x.id), - globals, - }, - }, - key: globals.event.uuid, - }) - } + const states = await this.hogWatcher.getStates(possibleInvocations.map((x) => x.hogFunction.id)) - hogFunctionsByState[HogWatcherState.disabledForPeriod]?.forEach((item) => { - this.produceAppMetric({ - team_id: item.team_id, - app_source_id: item.id, - metric_kind: 'failure', - metric_name: 'disabled_temporarily', - count: 1, - }) - }) + const overflowGlobalsAndFunctions: Record = {} - hogFunctionsByState[HogWatcherState.disabledIndefinitely]?.forEach((item) => { + const invocations = possibleInvocations.filter((item) => { + const state = states[item.hogFunction.id].state + if (state >= HogWatcherState.disabledForPeriod) { this.produceAppMetric({ - team_id: item.team_id, - app_source_id: item.id, + team_id: item.globals.project.id, + app_source_id: item.hogFunction.id, metric_kind: 'failure', - metric_name: 'disabled_permanently', + metric_name: + state === HogWatcherState.disabledForPeriod + ? 'disabled_temporarily' + : 'disabled_permanently', count: 1, }) - }) + return false + } - hogFunctionsByState[HogWatcherState.healthy]?.forEach((item) => { - invocations.push({ - globals, - hogFunction: item, - }) + if (state === HogWatcherState.degraded) { + const key = `${item.globals.project.id}-${item.globals.event.uuid}` + overflowGlobalsAndFunctions[key] = overflowGlobalsAndFunctions[key] || { + globals: item.globals, + hogFunctionIds: [], + } + + overflowGlobalsAndFunctions[key].hogFunctionIds.push(item.hogFunction.id) + counterFunctionInvocation.inc({ outcome: 'overflowed' }, 1) + + return false + } + + return true + }) + + Object.values(overflowGlobalsAndFunctions).forEach((item) => { + this.messagesToProduce.push({ + topic: KAFKA_CDP_FUNCTION_OVERFLOW, + value: { + source: 'event_invocations', + payload: item, + }, + key: item.globals.event.uuid, }) }) @@ -377,7 +401,7 @@ abstract class CdpConsumerBase { ) ).filter((x) => !!x) as HogFunctionInvocationResult[] - this.hogWatcher.currentObservations.observeResults(results) + await this.hogWatcher.observeResults(results) return results }, }) @@ -393,7 +417,7 @@ abstract class CdpConsumerBase { const globalConnectionConfig = createRdConnectionConfigFromEnvVars(this.hub) const globalProducerConfig = createRdProducerConfigFromEnvVars(this.hub) - await Promise.all([this.hogFunctionManager.start(), this.hogWatcher.start()]) + await Promise.all([this.hogFunctionManager.start()]) this.kafkaProducer = new KafkaProducerWrapper( await createKafkaProducer(globalConnectionConfig, globalProducerConfig) @@ -447,13 +471,12 @@ abstract class CdpConsumerBase { status.info('🔁', `${this.name} - stopping kafka producer`) await this.kafkaProducer?.disconnect() status.info('🔁', `${this.name} - stopping hog function manager and hog watcher`) - await Promise.all([this.hogFunctionManager.stop(), this.hogWatcher.stop()]) + await Promise.all([this.hogFunctionManager.stop()]) status.info('👍', `${this.name} - stopped!`) } public isHealthy() { - // TODO: Maybe extend this to check if we are shutting down so we don't get killed early. return this.batchConsumer?.isHealthy() } } @@ -598,9 +621,11 @@ export class CdpOverflowConsumer extends CdpConsumerBase { ) .flat() + const states = await this.hogWatcher.getStates(invocationGlobals.map((x) => x.hogFunctionIds).flat()) + const results = ( await this.runManyWithHeartbeat(invocations, (item) => { - const state = this.hogWatcher.getFunctionState(item.hogFunctionId) + const state = states[item.hogFunctionId].state if (state >= HogWatcherState.disabledForPeriod) { this.produceAppMetric({ team_id: item.globals.project.id, @@ -618,7 +643,7 @@ export class CdpOverflowConsumer extends CdpConsumerBase { }) ).filter((x) => !!x) as HogFunctionInvocationResult[] - this.hogWatcher.currentObservations.observeResults(results) + await this.hogWatcher.observeResults(results) return results }, }) diff --git a/plugin-server/src/cdp/hog-function-manager.ts b/plugin-server/src/cdp/hog-function-manager.ts index 17de15147441b..b09a35242542f 100644 --- a/plugin-server/src/cdp/hog-function-manager.ts +++ b/plugin-server/src/cdp/hog-function-manager.ts @@ -6,8 +6,10 @@ import { PubSub } from '../utils/pubsub' import { status } from '../utils/status' import { HogFunctionType, IntegrationType } from './types' -export type HogFunctionMap = Record -export type HogFunctionCache = Record +type HogFunctionCache = { + functions: Record + teams: Record +} const HOG_FUNCTION_FIELDS = ['id', 'team_id', 'name', 'enabled', 'inputs', 'inputs_schema', 'filters', 'bytecode'] @@ -21,7 +23,10 @@ export class HogFunctionManager { constructor(private postgres: PostgresRouter, private serverConfig: PluginsServerConfig) { this.started = false this.ready = false - this.cache = {} + this.cache = { + functions: {}, + teams: {}, + } this.pubSub = new PubSub(this.serverConfig, { 'reload-hog-functions': async (message) => { @@ -66,14 +71,27 @@ export class HogFunctionManager { if (!this.ready) { throw new Error('HogFunctionManager is not ready! Run HogFunctionManager.start() before this') } - return Object.values(this.cache[teamId] || {}) + + return Object.values(this.cache.teams[teamId] || []) + .map((id) => this.cache.functions[id]) + .filter((x) => !!x) as HogFunctionType[] + } + + public getHogFunction(id: HogFunctionType['id']): HogFunctionType | undefined { + if (!this.ready) { + throw new Error('HogFunctionManager is not ready! Run HogFunctionManager.start() before this') + } + return this.cache.functions[id] } public getTeamHogFunction(teamId: Team['id'], hogFunctionId: HogFunctionType['id']): HogFunctionType | undefined { if (!this.ready) { throw new Error('HogFunctionManager is not ready! Run HogFunctionManager.start() before this') } - return this.cache[teamId]?.[hogFunctionId] + const fn = this.cache.functions[hogFunctionId] + if (fn?.team_id === teamId) { + return fn + } } public teamHasHogFunctions(teamId: Team['id']): boolean { @@ -96,13 +114,15 @@ export class HogFunctionManager { await this.enrichWithIntegrations(items) - const cache: HogFunctionCache = {} - for (const item of items) { - if (!cache[item.team_id]) { - cache[item.team_id] = {} - } + const cache: HogFunctionCache = { + functions: {}, + teams: {}, + } - cache[item.team_id][item.id] = item + for (const item of items) { + cache.functions[item.id] = item + cache.teams[item.team_id] = cache.teams[item.team_id] || [] + cache.teams[item.team_id]!.push(item.id) } this.cache = cache @@ -125,17 +145,15 @@ export class HogFunctionManager { await this.enrichWithIntegrations(items) - if (!this.cache[teamId]) { - this.cache[teamId] = {} - } - for (const id of ids) { - // First of all delete the item from the cache - this covers the case where the item was deleted or disabled - delete this.cache[teamId][id] + delete this.cache.functions[id] + this.cache.teams[teamId] = this.cache.teams[teamId]?.filter((x) => x !== id) } for (const item of items) { - this.cache[teamId][item.id] = item + this.cache.functions[item.id] = item + this.cache.teams[teamId] = this.cache.teams[teamId] || [] + this.cache.teams[teamId]!.push(item.id) } } @@ -157,7 +175,7 @@ export class HogFunctionManager { public reloadIntegrations(teamId: Team['id'], ids: IntegrationType['id'][]): Promise { // We need to find all hog functions that depend on these integrations and re-enrich them - const items: HogFunctionType[] = Object.values(this.cache[teamId] || {}) + const items = this.getTeamHogFunctions(teamId) const itemsToReload = items.filter((item) => ids.some((id) => item.depends_on_integration_ids?.has(id))) return this.enrichWithIntegrations(itemsToReload) diff --git a/plugin-server/src/cdp/hog-watcher.ts b/plugin-server/src/cdp/hog-watcher.ts new file mode 100644 index 0000000000000..04b647613a0a0 --- /dev/null +++ b/plugin-server/src/cdp/hog-watcher.ts @@ -0,0 +1,334 @@ +import { captureException } from '@sentry/node' +import { Pipeline, Redis } from 'ioredis' + +import { Hub } from '../types' +import { timeoutGuard } from '../utils/db/utils' +import { now } from '../utils/now' +import { status } from '../utils/status' +import { UUIDT } from '../utils/utils' +import { HogFunctionInvocationResult, HogFunctionType } from './types' + +export const BASE_REDIS_KEY = process.env.NODE_ENV == 'test' ? '@posthog-test/hog-watcher' : '@posthog/hog-watcher' +const REDIS_KEY_TOKENS = `${BASE_REDIS_KEY}/tokens` +const REDIS_KEY_DISABLED = `${BASE_REDIS_KEY}/disabled` +const REDIS_KEY_DISABLED_HISTORY = `${BASE_REDIS_KEY}/disabled_history` +const REDIS_TIMEOUT_SECONDS = 5 + +// NOTE: We ideally would have this in a file but the current build step doesn't handle anything other than .ts files +const LUA_TOKEN_BUCKET = ` +local key = KEYS[1] +local now = ARGV[1] +local cost = ARGV[2] +local poolMax = ARGV[3] +local fillRate = ARGV[4] +local expiry = ARGV[5] +local before = redis.call('hget', key, 'ts') + +-- If we don't have a timestamp then we set it to now and fill up the bucket +if before == false then + local ret = poolMax - cost + redis.call('hset', key, 'ts', now) + redis.call('hset', key, 'pool', ret) + redis.call('expire', key, expiry) + return ret +end + +-- We update the timestamp if it has changed +local timeDiffSeconds = now - before + +if timeDiffSeconds > 0 then + redis.call('hset', key, 'ts', now) +else + timeDiffSeconds = 0 +end + +-- Calculate how much should be refilled in the bucket and add it +local owedTokens = timeDiffSeconds * fillRate +local currentTokens = redis.call('hget', key, 'pool') + +if currentTokens == false then + currentTokens = poolMax +end + +currentTokens = math.min(currentTokens + owedTokens, poolMax) + +-- Remove the cost and return the new number of tokens +if currentTokens - cost >= 0 then + currentTokens = currentTokens - cost +else + currentTokens = -1 +end + +redis.call('hset', key, 'pool', currentTokens) +redis.call('expire', key, expiry) + +-- Finally return the value - if it's negative then we've hit the limit +return currentTokens +` + +export enum HogWatcherState { + healthy = 1, + degraded = 2, + disabledForPeriod = 3, + disabledIndefinitely = 4, +} + +export type HogWatcherFunctionState = { + state: HogWatcherState + tokens: number + rating: number +} + +type WithCheckRateLimit = { + checkRateLimit: (key: string, now: number, cost: number, poolMax: number, fillRate: number, expiry: number) => T +} + +type HogWatcherRedisClientPipeline = Pipeline & WithCheckRateLimit + +type HogWatcherRedisClient = Omit & + WithCheckRateLimit> & { + pipeline: () => HogWatcherRedisClientPipeline + } + +export class HogWatcher { + constructor(private hub: Hub, private onStateChange: (id: HogFunctionType['id'], state: HogWatcherState) => void) {} + + private rateLimitArgs(id: HogFunctionType['id'], cost: number) { + const nowSeconds = Math.round(now() / 1000) + return [ + `${REDIS_KEY_TOKENS}/${id}`, + nowSeconds, + cost, + this.hub.CDP_WATCHER_BUCKET_SIZE, + this.hub.CDP_WATCHER_REFILL_RATE, + this.hub.CDP_WATCHER_TTL, + ] as const + } + + private async runRedis(fn: (client: HogWatcherRedisClient) => Promise): Promise { + // We want all of this to fail open in the issue of redis being unavailable - we'd rather have the function continue + const client = await this.hub.redisPool.acquire() + + client.defineCommand('checkRateLimit', { + numberOfKeys: 1, + lua: LUA_TOKEN_BUCKET, + }) + + const timeout = timeoutGuard( + `Redis call delayed. Waiting over ${REDIS_TIMEOUT_SECONDS} seconds.`, + undefined, + REDIS_TIMEOUT_SECONDS * 1000 + ) + try { + return await fn(client as HogWatcherRedisClient) + } catch (e) { + status.error('HogWatcher Redis error', e) + captureException(e) + return null + } finally { + clearTimeout(timeout) + await this.hub.redisPool.release(client) + } + } + + public tokensToFunctionState(tokens?: number | null, stateOverride?: HogWatcherState): HogWatcherFunctionState { + tokens = tokens ?? this.hub.CDP_WATCHER_BUCKET_SIZE + const rating = tokens / this.hub.CDP_WATCHER_BUCKET_SIZE + + const state = + stateOverride ?? + (rating >= this.hub.CDP_WATCHER_THRESHOLD_DEGRADED + ? HogWatcherState.healthy + : rating > 0 + ? HogWatcherState.degraded + : HogWatcherState.disabledForPeriod) + + return { state, tokens, rating } + } + + public async getStates( + ids: HogFunctionType['id'][] + ): Promise> { + const idsSet = new Set(ids) + + const res = await this.runRedis(async (client) => { + const pipeline = client.pipeline() + + for (const id of idsSet) { + pipeline.checkRateLimit(...this.rateLimitArgs(id, 0)) + pipeline.get(`${REDIS_KEY_DISABLED}/${id}`) + pipeline.ttl(`${REDIS_KEY_DISABLED}/${id}`) + } + + return pipeline.exec() + }) + + return Array.from(idsSet).reduce((acc, id, index) => { + const resIndex = index * 3 + const tokens = res ? res[resIndex][1] : undefined + const disabled = res ? res[resIndex + 1][1] : false + const disabledTemporarily = disabled && res ? res[resIndex + 2][1] !== -1 : false + + return { + ...acc, + [id]: this.tokensToFunctionState( + tokens, + disabled + ? disabledTemporarily + ? HogWatcherState.disabledForPeriod + : HogWatcherState.disabledIndefinitely + : undefined + ), + } + }, {} as Record) + } + + public async getState(id: HogFunctionType['id']): Promise { + const res = await this.getStates([id]) + return res[id] + } + + public async forceStateChange(id: HogFunctionType['id'], state: HogWatcherState): Promise { + await this.runRedis(async (client) => { + const pipeline = client.pipeline() + + const newScore = + state === HogWatcherState.healthy + ? this.hub.CDP_WATCHER_BUCKET_SIZE + : state === HogWatcherState.degraded + ? this.hub.CDP_WATCHER_BUCKET_SIZE * this.hub.CDP_WATCHER_THRESHOLD_DEGRADED + : 0 + + const nowSeconds = Math.round(now() / 1000) + + pipeline.hset(`${REDIS_KEY_TOKENS}/${id}`, 'pool', newScore) + pipeline.hset(`${REDIS_KEY_TOKENS}/${id}`, 'ts', nowSeconds) + + if (state === HogWatcherState.disabledForPeriod) { + pipeline.set(`${REDIS_KEY_DISABLED}/${id}`, '1', 'EX', this.hub.CDP_WATCHER_DISABLED_TEMPORARY_TTL) + } else if (state === HogWatcherState.disabledIndefinitely) { + pipeline.set(`${REDIS_KEY_DISABLED}/${id}`, '1') + } else { + pipeline.del(`${REDIS_KEY_DISABLED}/${id}`) + } + + await pipeline.exec() + }) + + this.onStateChange(id, state) + } + + public async observeResults(results: HogFunctionInvocationResult[]): Promise { + const costs: Record = {} + + results.forEach((result) => { + let cost = (costs[result.invocation.hogFunctionId] = costs[result.invocation.hogFunctionId] || 0) + + if (result.finished) { + // If it is finished we can calculate the score based off of the timings + + const totalDurationMs = result.invocation.timings.reduce((acc, timing) => acc + timing.duration_ms, 0) + const lowerBound = this.hub.CDP_WATCHER_COST_TIMING_LOWER_MS + const upperBound = this.hub.CDP_WATCHER_COST_TIMING_UPPER_MS + const costTiming = this.hub.CDP_WATCHER_COST_TIMING + const ratio = Math.max(totalDurationMs - lowerBound, 0) / (upperBound - lowerBound) + + cost += Math.round(costTiming * ratio) + } + + if (result.error) { + cost += this.hub.CDP_WATCHER_COST_ERROR + } + + costs[result.invocation.hogFunctionId] = cost + }) + + const res = await this.runRedis(async (client) => { + const pipeline = client.pipeline() + + Object.entries(costs).forEach(([id, change]) => { + pipeline.checkRateLimit(...this.rateLimitArgs(id, change)) + }) + + return await pipeline.exec() + }) + + // TRICKY: the above part is straight forward - below is more complex as we do multiple calls to ensure + // that we disable the function temporarily and eventually permanently. As this is only called when the function + // transitions to a disabled state, it is not a performance concern. + + const disabledFunctionIds = Object.entries(costs) + .filter((_, index) => (res ? res[index][1] <= 0 : false)) + .map(([id]) => id) + + if (disabledFunctionIds.length) { + // Mark them all as disabled in redis + + const results = await this.runRedis(async (client) => { + const pipeline = client.pipeline() + + disabledFunctionIds.forEach((id) => { + pipeline.set( + `${REDIS_KEY_DISABLED}/${id}`, + '1', + 'EX', + this.hub.CDP_WATCHER_DISABLED_TEMPORARY_TTL, + 'NX' + ) + }) + + return pipeline.exec() + }) + + const functionsTempDisabled = disabledFunctionIds.filter((_, index) => + results ? results[index][1] : false + ) + + if (!functionsTempDisabled.length) { + return + } + + // We store the history as a zset - we can then use it to determine if we should disable indefinitely + const historyResults = await this.runRedis(async (client) => { + const pipeline = client.pipeline() + functionsTempDisabled.forEach((id) => { + const key = `${REDIS_KEY_DISABLED_HISTORY}/${id}` + pipeline.zadd(key, now(), new UUIDT().toString()) + pipeline.zrange(key, 0, -1) + pipeline.expire(key, this.hub.CDP_WATCHER_TTL) + }) + + return await pipeline.exec() + }) + + const functionsToDisablePermanently = functionsTempDisabled.filter((_, index) => { + const history = historyResults ? historyResults[index * 3 + 1][1] : [] + return history.length >= this.hub.CDP_WATCHER_DISABLED_TEMPORARY_MAX_COUNT + }) + + if (functionsToDisablePermanently.length) { + await this.runRedis(async (client) => { + const pipeline = client.pipeline() + functionsToDisablePermanently.forEach((id) => { + const key = `${REDIS_KEY_DISABLED}/${id}` + pipeline.set(key, '1') + pipeline.del(`${REDIS_KEY_DISABLED_HISTORY}/${id}`) + }) + + return await pipeline.exec() + }) + } + + // Finally track the results + functionsToDisablePermanently.forEach((id) => { + this.onStateChange(id, HogWatcherState.disabledIndefinitely) + }) + + functionsTempDisabled.forEach((id) => { + if (!functionsToDisablePermanently.includes(id)) { + this.onStateChange(id, HogWatcherState.disabledForPeriod) + } + }) + } + } +} diff --git a/plugin-server/src/cdp/hog-watcher/README.md b/plugin-server/src/cdp/hog-watcher/README.md deleted file mode 100644 index 214cd41c9ec16..0000000000000 --- a/plugin-server/src/cdp/hog-watcher/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# How this whole thing works - -The HogWatcher is a class that is responsible for monitoring the health of the hog functions. -Generally we want to make "observations" about the health of a function and then based on those observations we can determine the "state" of the function. -Observations are made per-consumer and then aggregated by the leader to determine the state of the function. - -Each Watcher only needs to worry about the current state of any functions it is processing. The observations are only really interesting to the leader, as it -is the one that will be making the decisions about the state of the function. - -# Rating system - -We want to detect when a function has gone rogue and gradually stop it from running. -We calculate its "rating" based on how many times it has succeeded and failed. - -- If the rating falls too low, over a period of time we want to move it to the overflow queue as a first step to ensure it doesn't hog resources. -- If it stays too low, we eventually want to disable it for a period of time. -- If it _still_ behaves poorly after this time period, we want to disable it indefinitely. - -This can be represented as a state for the function - 1. Healthy, 2. Overflowed, 3. Disabled for a period, 4. Disabled indefinitely. - -To be able to do this right we need to store an array of values for the functions rating over time that represent the last say 10 minutes. - -In addition we need to record the last N states of the function so that we can decide to disable it indefinitely if it has spent too much time in state 3 - -- State 1: - - If the rating average over the time period is below 0.5, move to state 2. -- State 2: - - If the rating average over the time period is above 0.5, move to state 1. - - If the rating average over the time period is below 0.5 AND the function was in state 3 for more than N of the last states, move to state 4. - - If the rating average over the time period is below 0.5, move to state 3. -- State 3: - - The function is disabled for a period of time (perhaps the same as the measuring period). - - Once it is out of this masked period, move to state 2. -- State 4: - - The function is disabled and requires manual intervention - -# Leader specific work - -To simplify an already relatively complex concept, there is one leader who is responsible for making sure the persisted state is efficient and up-to-date. -It is also responsible for calculating state changes, persisting them to redis and emitting to other consumers. - -The state is kept in one redis @hash with keys like this: - -```js -{ - "states": `[["a", 1], ["b", 2]]`, - "FUNCTION_ID:states": `[{ t: 1, s: 0.5 }]`, - "FUNCTION_ID:ratings": `[{ t: 1, r: 0.9 }]`, - "FUNCTION_ID:observation:observerID:periodTimestamp": `[{ s: 1, f: 2, as: 0, af: 1 }]` -} -``` - -Whenever an observation is made, it is persisted to a temporary key `FUNCTION_ID:observation:observerID:timestamp` and emitted. This allows the leader to load it from redis on startup as well as react to the emitted value, whichever it does first. -Periodically it merges all observations together and when a finally rating is calculated it persists just the rating (to save on space). - -At the same time as compacting the ratings, it checks for any state changes and updates the relevant state key. - -This is designed to keep the workers lightweight, only having to worry about their own observations and keeping a list of states in memory. Only the leader has to keep the whole object in memory. diff --git a/plugin-server/src/cdp/hog-watcher/hog-watcher.ts b/plugin-server/src/cdp/hog-watcher/hog-watcher.ts deleted file mode 100644 index 3b675a44d48a1..0000000000000 --- a/plugin-server/src/cdp/hog-watcher/hog-watcher.ts +++ /dev/null @@ -1,582 +0,0 @@ -import { randomUUID } from 'node:crypto' -import { Counter } from 'prom-client' - -import { CdpConfig, Hub } from '../../types' -import { PubSub } from '../../utils/pubsub' -import { status } from '../../utils/status' -import { HogFunctionInvocationAsyncResponse, HogFunctionInvocationResult, HogFunctionType } from '../types' -import { - EmittedHogWatcherObservations, - EmittedHogWatcherStates, - HogWatcherGlobalState, - HogWatcherObservationPeriod, - HogWatcherObservationPeriodWithInstanceId, - HogWatcherRatingPeriod, - HogWatcherState, - HogWatcherStatePeriod, - HogWatcherSummary, -} from './types' -import { - BASE_REDIS_KEY, - calculateRating, - deriveCurrentStateFromRatings, - last, - periodTimestamp, - runRedis, - stripFalsey, -} from './utils' - -const REDIS_KEY_STATE = `${BASE_REDIS_KEY}/state` - -const hogStateChangeCounter = new Counter({ - name: 'cdp_hog_watcher_state_change', - help: 'An function was moved to a different state', - labelNames: ['state'], -}) - -export class HogWatcherActiveObservations { - observations: Record = {} - - constructor(private config: CdpConfig) {} - - private addObservations( - id: HogFunctionType['id'], - incrs: Pick< - Partial, - 'successes' | 'failures' | 'asyncFunctionFailures' | 'asyncFunctionSuccesses' - > - ): void { - if (!this.observations[id]) { - this.observations[id] = { - timestamp: periodTimestamp(this.config), - successes: 0, - failures: 0, - asyncFunctionFailures: 0, - asyncFunctionSuccesses: 0, - } - } - - this.observations[id].successes += incrs.successes ?? 0 - this.observations[id].failures += incrs.failures ?? 0 - this.observations[id].asyncFunctionFailures += incrs.asyncFunctionFailures ?? 0 - this.observations[id].asyncFunctionSuccesses += incrs.asyncFunctionSuccesses ?? 0 - } - - observeResults(results: HogFunctionInvocationResult[]) { - results.forEach((result) => - this.addObservations(result.invocation.hogFunctionId, { - successes: result.finished ? 1 : 0, - failures: result.error ? 1 : 0, - }) - ) - } - - observeAsyncFunctionResponses(responses: HogFunctionInvocationAsyncResponse[]) { - // NOTE: This probably wants to be done using the response status instead :thinking: - responses.forEach((response) => - this.addObservations(response.hogFunctionId, { - asyncFunctionSuccesses: response.asyncFunctionResponse.error ? 0 : 1, - asyncFunctionFailures: response.asyncFunctionResponse.error ? 1 : 0, - }) - ) - } -} - -export class HogWatcher { - public readonly currentObservations: HogWatcherActiveObservations - public states: Record = {} - private queuedManualStates: Record = {} - - // Only set if we are the leader - public globalState?: HogWatcherGlobalState - // Only the leader should be able to write to the states - public isLeader: boolean = false - private pubSub: PubSub - private instanceId: string - private syncTimer?: NodeJS.Timeout - - constructor(private hub: Hub) { - this.currentObservations = new HogWatcherActiveObservations(hub) - - this.instanceId = randomUUID() - this.pubSub = new PubSub(hub, { - 'hog-watcher-states': (message) => { - const { states }: EmittedHogWatcherStates = JSON.parse(message) - - this.states = { - ...this.states, - ...states, - } - }, - - 'hog-watcher-observations': (message) => { - // We only care about observations from other instances if we have a global state already loaded - if (!this.globalState) { - return - } - - const { instanceId, observations }: EmittedHogWatcherObservations = JSON.parse(message) - - observations.forEach(({ id, observation }) => { - const items = (this.globalState!.observations[id] = this.globalState!.observations[id] ?? []) - items.push({ - ...observation, - instanceId: instanceId, - }) - }) - }, - - 'hog-watcher-user-state-change': (message) => { - if (!this.isLeader) { - return - } - - const { states }: EmittedHogWatcherStates = JSON.parse(message) - - Object.entries(states).forEach(([id, state]) => { - this.queuedManualStates[id] = state - }) - - void this.syncLoop() - }, - }) - } - - async start() { - await this.pubSub.start() - - // Get the initial state of the watcher - await this.syncStates() - - if (process.env.NODE_ENV === 'test') { - // Not setting up loop in test mode - return - } - - await this.syncLoop() - } - - async stop() { - await this.pubSub.stop() - - if (this.syncTimer) { - clearTimeout(this.syncTimer) - } - if (!this.isLeader) { - return - } - - await runRedis(this.hub.redisPool, 'stop', async (client) => { - return client.del(`${BASE_REDIS_KEY}/leader`) - }) - - await this.flushActiveObservations() - } - - public getFunctionState(id: HogFunctionType['id']): HogWatcherState { - return this.states[id] ?? HogWatcherState.healthy - } - - private async checkIsLeader() { - const leaderId = await runRedis(this.hub.redisPool, 'getLeader', async (client) => { - // Set the leader to this instance if it is not set and add an expiry to it of twice our observation period - const pipeline = client.pipeline() - - // TODO: This can definitely be done in a single command - just need to make sure the ttl is always extended if the ID is the same - - pipeline.set( - `${BASE_REDIS_KEY}/leader`, - this.instanceId, - 'NX', - // @ts-expect-error - IORedis types don't allow for NX and EX in the same command - 'EX', - (this.hub.CDP_WATCHER_OBSERVATION_PERIOD * 3) / 1000 - ) - pipeline.get(`${BASE_REDIS_KEY}/leader`) - const [_, res] = await pipeline.exec() - - // NOTE: IORedis types don't allow for NX and GET in the same command so we have to cast it to any - return res[1] as string - }) - - this.isLeader = leaderId === this.instanceId - - if (this.isLeader) { - status.info('👀', '[HogWatcher] I am the leader') - } - } - - public syncLoop = async () => { - clearTimeout(this.syncTimer) - try { - await this.sync() - } finally { - this.syncTimer = setTimeout(() => this.syncLoop(), this.hub.CDP_WATCHER_OBSERVATION_PERIOD) - } - } - - public async sync() { - await this.checkIsLeader() - await this.flushActiveObservations() - - if (this.isLeader) { - await this.syncState() - } else { - // Clear any states that are only relevant to the leader - this.globalState = undefined - this.queuedManualStates = {} - } - } - - private async flushActiveObservations() { - const changes: EmittedHogWatcherObservations = { - instanceId: this.instanceId, - observations: [], - } - - const period = periodTimestamp(this.hub) - - Object.entries(this.currentObservations.observations).forEach(([id, observation]) => { - if (observation.timestamp !== period) { - changes.observations.push({ id, observation }) - delete this.currentObservations.observations[id] - } - }) - - if (!changes.observations.length) { - return - } - - // Write all the info to redis - await runRedis(this.hub.redisPool, 'syncWithRedis', async (client) => { - const pipeline = client.pipeline() - - changes.observations.forEach(({ id, observation }) => { - // We key the observations by observerId and timestamp with a ttl of the max period we want to keep the data for - const subKey = `observation:${id}:${this.instanceId}:${observation.timestamp}` - pipeline.hset(REDIS_KEY_STATE, subKey, Serializer.serializeObservation(observation)) - }) - - return pipeline.exec() - }) - - // Now we can emit to the others so they can update their state - await this.pubSub.publish('hog-watcher-observations', JSON.stringify(changes)) - } - - private async syncState() { - // Flushing states involves a couple of things and is only done by the leader to avoid clashes - - // 1. Prune old states that are no longer relevant (we only keep the last N states) - // 2. Calculate the state for each function based on their existing observations and previous states - // 3. If the state has changed, write it to redis and emit it to the others - - if (!this.isLeader) { - status.warn('👀', '[HogWatcher] Only the leader can flush states') - return - } - - const globalState = (this.globalState = this.globalState ?? (await this.fetchState())) - - const stateChanges: EmittedHogWatcherStates = { - instanceId: this.instanceId, - states: {}, - } - - // We want to gather all observations that are at least 1 period older than the current period - // That gives enough time for each worker to have pushed out their observations - - const period = periodTimestamp(this.hub) - const keysToRemove: string[] = [] - const changedHogFunctionRatings = new Set() - const RATINGS_PERIOD_MASK = this.hub.CDP_WATCHER_OBSERVATION_PERIOD * 2 - - // Group the observations by functionId and timestamp and generate their rating - Object.entries(globalState.observations).forEach(([id, observations]) => { - const groupedByTimestamp: Record = {} - const [oldEnoughObservations, others] = observations.reduce( - (acc, observation) => { - if (observation.timestamp <= period - RATINGS_PERIOD_MASK) { - // Add the key to be removed from redis later - keysToRemove.push(`observation:${id}:${observation.instanceId}:${observation.timestamp}`) - acc[0].push(observation) - } else { - acc[1].push(observation) - } - return acc - }, - [[], []] as [HogWatcherObservationPeriodWithInstanceId[], HogWatcherObservationPeriodWithInstanceId[]] - ) - - // Keep only the observations that aren't ready to be persisted - if (others.length) { - globalState.observations[id] = others - } else { - delete globalState.observations[id] - } - - // Group them all by timestamp to generate a new rating - oldEnoughObservations.forEach((observation) => { - const key = `${id}:${observation.timestamp}` - groupedByTimestamp[key] = groupedByTimestamp[key] ?? { - timestamp: observation.timestamp, - successes: 0, - failures: 0, - asyncFunctionSuccesses: 0, - asyncFunctionFailures: 0, - } - groupedByTimestamp[key].successes += observation.successes - groupedByTimestamp[key].failures += observation.failures - groupedByTimestamp[key].asyncFunctionSuccesses += observation.asyncFunctionSuccesses - groupedByTimestamp[key].asyncFunctionFailures += observation.asyncFunctionFailures - }) - - Object.entries(groupedByTimestamp).forEach(([_, observation]) => { - const rating = calculateRating(observation) - globalState.ratings[id] = globalState.ratings[id] ?? [] - globalState.ratings[id].push({ timestamp: observation.timestamp, rating: rating }) - globalState.ratings[id] = globalState.ratings[id].slice(-this.hub.CDP_WATCHER_MAX_RECORDED_RATINGS) - - changedHogFunctionRatings.add(id) - }) - }) - - const transitionToState = (id: HogFunctionType['id'], newState: HogWatcherState) => { - const state: HogWatcherStatePeriod = { - timestamp: periodTimestamp(this.hub), - state: newState, - } - - globalState.states[id] = globalState.states[id] ?? [] - globalState.states[id].push(state) - globalState.states[id] = globalState.states[id].slice(-this.hub.CDP_WATCHER_MAX_RECORDED_STATES) - stateChanges.states[id] = newState - hogStateChangeCounter.inc({ state: newState }) - } - - changedHogFunctionRatings.forEach((id) => { - // Build the new ratings to be written - // Check if the state has changed and if so add it to the list of changes - const newRatings = globalState.ratings[id] - const currentState = last(globalState.states[id])?.state ?? HogWatcherState.healthy - const newState = deriveCurrentStateFromRatings(this.hub, newRatings, globalState.states[id] ?? []) - - if (currentState !== newState) { - transitionToState(id, newState) - // Extra logging to help debugging: - - status.info('👀', `[HogWatcher] Function ${id} changed state`, { - oldState: currentState, - newState: newState, - ratings: newRatings, - }) - } - }) - - // In addition we need to check temporarily disabled functions and move them back to overflow if they are behaving well - Object.entries(globalState.states).forEach(([id, states]) => { - const currentState = last(states)?.state - if (currentState === HogWatcherState.disabledForPeriod) { - // Also check the state change here - const newState = deriveCurrentStateFromRatings(this.hub, globalState.ratings[id] ?? [], states) - - if (newState !== currentState) { - transitionToState(id, newState) - } - } - }) - - // Finally we make sure any manual changes that came in are applied - Object.entries(this.queuedManualStates).forEach(([id, state]) => { - transitionToState(id, state) - delete this.queuedManualStates[id] - }) - - if (!changedHogFunctionRatings.size && !Object.keys(stateChanges.states).length) { - // Nothing to do - return - } - - if (Object.keys(stateChanges.states).length) { - status.info('👀', '[HogWatcher] Functions changed state', { - changes: stateChanges, - }) - } - - // Finally write the state summary - const states: Record = Object.fromEntries( - Object.entries(globalState.states).map(([id, states]) => [id, last(states)!.state]) - ) - - // Finally we write the changes to redis and emit them to the others - await runRedis(this.hub.redisPool, 'syncWithRedis', async (client) => { - const pipeline = client.pipeline() - - // Remove old observations - keysToRemove.forEach((key) => { - pipeline.hdel(REDIS_KEY_STATE, key) - }) - - // Write the new ratings - changedHogFunctionRatings.forEach((id) => { - const ratings = globalState.ratings[id] ?? [] - pipeline.hset(REDIS_KEY_STATE, `ratings:${id}`, Serializer.serializeRatings(ratings)) - }) - - Object.keys(stateChanges.states).forEach((id) => { - const states = globalState.states[id] ?? [] - pipeline.hset(REDIS_KEY_STATE, `states:${id}`, Serializer.serializeStates(states)) - }) - - // Write the new states - pipeline.hset(REDIS_KEY_STATE, 'states', Serializer.serializeAllStates(states)) - - return pipeline.exec() - }) - - // // Now we can emit to the others so they can update their state - await this.pubSub.publish('hog-watcher-states', JSON.stringify(stateChanges)) - } - - async syncStates(): Promise> { - const res = await runRedis(this.hub.redisPool, 'fetchWatcher', async (client) => { - return client.hget(REDIS_KEY_STATE, 'states') - }) - - this.states = res ? Serializer.deserializeAllStates(res) : {} - - return this.states - } - - /** - * Fetch the summary for HogFunction (used by the UI, hence no caching) - */ - async fetchWatcher(id: HogFunctionType['id']): Promise { - const [statesStr, ratingsStr] = await runRedis(this.hub.redisPool, 'fetchWatcher', async (client) => { - return client.hmget(REDIS_KEY_STATE, `states:${id}`, `ratings:${id}`) - }) - - const states: HogWatcherStatePeriod[] = statesStr ? Serializer.deserializeStates(statesStr) : [] - const ratings: HogWatcherRatingPeriod[] = ratingsStr ? Serializer.deserializeRatings(ratingsStr) : [] - - return { - state: last(states)?.state ?? HogWatcherState.healthy, - states: states, - ratings: ratings, - } - } - - async forceStateChange(id: HogFunctionType['id'], state: HogWatcherState): Promise { - // Ensure someone is the leader - await this.checkIsLeader() - const changes: EmittedHogWatcherStates = { - instanceId: this.instanceId, - states: { - [id]: state, - }, - } - - await this.pubSub.publish('hog-watcher-user-state-change', JSON.stringify(changes)) - } - - /** - * Fetch the entire state object parsing into a usable object - */ - async fetchState(): Promise { - const redisState = await runRedis(this.hub.redisPool, 'fetchWatcher', async (client) => { - return client.hgetall(REDIS_KEY_STATE) - }) - - return Serializer.deserializeGlobalState(redisState) - } -} - -class Serializer { - // Serializer to help parsing back and forth to redis - mostly focused on reducing the size of the stored values - - static deserializeGlobalState(redisState: Record): HogWatcherGlobalState { - const response: HogWatcherGlobalState = { - states: {}, - ratings: {}, - observations: {}, - } - - Object.entries(redisState).forEach(([key, value]) => { - const [kind, id, ...rest] = key.split(':') - if (kind === 'states' && id) { - response.states[id] = this.deserializeStates(value) - } else if (kind === 'ratings') { - response.ratings[id] = this.deserializeRatings(value) - } else if (kind === 'observation') { - const [instanceId, timestamp] = rest - const partial = this.deserializeObservation(value) - const observations: HogWatcherObservationPeriodWithInstanceId[] = (response.observations[id] = - response.observations[id] ?? []) - - observations.push({ - ...partial, - instanceId: instanceId, - timestamp: parseInt(timestamp), - }) - } else if (kind === 'states') { - // We can ignore this as it is the global state - } else { - status.warn('👀', `Unknown key kind ${kind} in fetchState`) - } - }) - - return response - } - - static serializeAllStates(val: Record): string { - const obj = Object.entries(val).map(([id, state]) => [id, state]) - return JSON.stringify(obj) - } - - static deserializeAllStates(val: string): Record { - const obj: (string | HogWatcherState)[][] = JSON.parse(val) - return Object.fromEntries(obj) - } - - static serializeStates(val: HogWatcherStatePeriod[]): string { - const obj = val.map((x) => ({ t: x.timestamp, s: x.state })) - return JSON.stringify(obj) - } - - static deserializeStates(val: string): HogWatcherStatePeriod[] { - const obj = JSON.parse(val) - return obj.map((x: { t: number; s: HogWatcherState }) => ({ timestamp: x.t, state: x.s })) - } - - static serializeRatings(val: HogWatcherRatingPeriod[]): string { - const obj = val.map((x) => ({ t: x.timestamp, r: x.rating })) - return JSON.stringify(obj) - } - - static deserializeRatings(val: string): HogWatcherRatingPeriod[] { - const obj = JSON.parse(val) - return obj.map((x: { t: number; r: number }) => ({ timestamp: x.t, rating: x.r })) - } - - static serializeObservation(val: HogWatcherObservationPeriod): string { - const obj = stripFalsey({ - t: val.timestamp, - s: val.successes, - f: val.failures, - af: val.asyncFunctionFailures, - as: val.asyncFunctionSuccesses, - }) - return JSON.stringify(obj) - } - - static deserializeObservation(val: string): HogWatcherObservationPeriod { - const obj = JSON.parse(val) - return { - timestamp: obj.t, - successes: obj.s ?? 0, - failures: obj.f ?? 0, - asyncFunctionFailures: obj.af ?? 0, - asyncFunctionSuccesses: obj.as ?? 0, - } - } -} diff --git a/plugin-server/src/cdp/hog-watcher/types.ts b/plugin-server/src/cdp/hog-watcher/types.ts deleted file mode 100644 index e145f0d0dff2d..0000000000000 --- a/plugin-server/src/cdp/hog-watcher/types.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { HogFunctionType } from '../types' - -export enum HogWatcherState { - healthy = 1, - overflowed = 2, - disabledForPeriod = 3, - disabledIndefinitely = 4, -} - -export type HogWatcherStatePeriod = { - timestamp: number - state: HogWatcherState -} - -export type HogWatcherRatingPeriod = { - timestamp: number - rating: number -} - -export type HogWatcherObservationPeriod = { - timestamp: number - successes: number - failures: number - asyncFunctionFailures: number - asyncFunctionSuccesses: number -} - -export type HogWatcherObservationPeriodWithInstanceId = HogWatcherObservationPeriod & { - instanceId: string -} - -export type HogWatcherSummary = { - state: HogWatcherState - states: HogWatcherStatePeriod[] - ratings: HogWatcherRatingPeriod[] -} - -export type EmittedHogWatcherObservations = { - instanceId: string - observations: { - id: HogFunctionType['id'] - observation: HogWatcherObservationPeriod - }[] -} - -export type EmittedHogWatcherStates = { - instanceId: string - states: { - [key: HogFunctionType['id']]: HogWatcherState - } -} - -// Deserialized version of what is stored in redis -export type HogWatcherGlobalState = { - /** Summary of all state history for every function */ - states: Record - /** Summary of all rating history for all functions */ - ratings: Record - /** All in progress observations that have not been serialized into ratings */ - observations: Record -} diff --git a/plugin-server/src/cdp/hog-watcher/utils.ts b/plugin-server/src/cdp/hog-watcher/utils.ts deleted file mode 100644 index f5c372c39e0ea..0000000000000 --- a/plugin-server/src/cdp/hog-watcher/utils.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Redis } from 'ioredis' - -import { CdpConfig, RedisPool } from '../../types' -import { timeoutGuard } from '../../utils/db/utils' -import { now } from '../../utils/now' -import { HogWatcherObservationPeriod, HogWatcherRatingPeriod, HogWatcherState, HogWatcherStatePeriod } from './types' - -const REDIS_TIMEOUT_SECONDS = 5 -export const BASE_REDIS_KEY = process.env.NODE_ENV == 'test' ? '@posthog-test/hog-watcher' : '@posthog/hog-watcher' - -export const calculateRating = (observation: HogWatcherObservationPeriod): number => { - // Rating is from 0 to 1 - // 1 - Function is working perfectly - // 0 - Function is not working at all - - // NOTE: Once we have proper async function support we should likely change the rating system to penalize slow requests more - // Also the function timing out should be penalized heavily as it indicates bad code (infinite loops etc.) - - const totalInvocations = observation.successes + observation.failures - const totalAsyncInvocations = observation.asyncFunctionSuccesses + observation.asyncFunctionFailures - const successRate = totalInvocations ? observation.successes / totalInvocations : 1 - const asyncSuccessRate = totalAsyncInvocations ? observation.asyncFunctionSuccesses / totalAsyncInvocations : 1 - - return Math.min(1, successRate, asyncSuccessRate) -} - -export const periodTimestamp = (config: CdpConfig, timestamp?: number): number => { - // Returns the timestamp but rounded to the nearest period (e.g. 1 minute) - const period = config.CDP_WATCHER_OBSERVATION_PERIOD - return Math.floor((timestamp ?? now()) / period) * period -} - -/** - * Calculate what the state should be based on the previous rating and states - */ -export const deriveCurrentStateFromRatings = ( - config: CdpConfig, - ratings: HogWatcherRatingPeriod[], - states: HogWatcherStatePeriod[] -): HogWatcherState => { - const { - CDP_WATCHER_OBSERVATION_PERIOD, - CDP_WATCHER_MAX_RECORDED_RATINGS, - CDP_WATCHER_DISABLED_PERIOD, - CDP_WATCHER_MIN_OBSERVATIONS, - CDP_WATCHER_OVERFLOW_RATING_THRESHOLD, - CDP_WATCHER_DISABLED_RATING_THRESHOLD, - CDP_WATCHER_MAX_ALLOWED_TEMPORARY_DISABLED, - } = config - const currentState = states[states.length - 1] ?? { - // Set the timestamp back far enough that all ratings are included - timestamp: now() - CDP_WATCHER_OBSERVATION_PERIOD * CDP_WATCHER_MAX_RECORDED_RATINGS, - state: HogWatcherState.healthy, - } - - if (currentState.state === HogWatcherState.disabledIndefinitely) { - return HogWatcherState.disabledIndefinitely - } - - // If we are disabled for a period then we only check if it should no longer be disabled - if (currentState.state === HogWatcherState.disabledForPeriod) { - if (now() - currentState.timestamp > CDP_WATCHER_DISABLED_PERIOD) { - return HogWatcherState.overflowed - } - } - - const ratingsSinceLastState = ratings.filter((x) => x.timestamp >= currentState.timestamp) - - if (ratingsSinceLastState.length < CDP_WATCHER_MIN_OBSERVATIONS) { - // We need to give the function a chance to run before we can evaluate it - return currentState.state - } - - const averageRating = ratingsSinceLastState.reduce((acc, x) => acc + x.rating, 0) / ratingsSinceLastState.length - - if (currentState.state === HogWatcherState.overflowed) { - if (averageRating > CDP_WATCHER_OVERFLOW_RATING_THRESHOLD) { - // The function is behaving well again - move it to healthy - return HogWatcherState.healthy - } - - if (averageRating < CDP_WATCHER_DISABLED_RATING_THRESHOLD) { - // The function is behaving worse than overflow can accept - disable it - const disabledStates = states.filter((x) => x.state === HogWatcherState.disabledForPeriod) - - if (disabledStates.length >= CDP_WATCHER_MAX_ALLOWED_TEMPORARY_DISABLED) { - // this function has spent half of the time in temporary disabled so we disable it indefinitely - return HogWatcherState.disabledIndefinitely - } - - return HogWatcherState.disabledForPeriod - } - } - - if (currentState.state === HogWatcherState.healthy) { - if (averageRating < CDP_WATCHER_OVERFLOW_RATING_THRESHOLD) { - return HogWatcherState.overflowed - } - } - - return currentState.state -} - -export async function runRedis( - redisPool: RedisPool, - description: string, - fn: (client: Redis) => Promise -): Promise { - const client = await redisPool.acquire() - const timeout = timeoutGuard( - `${description} delayed. Waiting over ${REDIS_TIMEOUT_SECONDS} seconds.`, - undefined, - REDIS_TIMEOUT_SECONDS * 1000 - ) - try { - return await fn(client) - } finally { - clearTimeout(timeout) - await redisPool.release(client) - } -} - -export function last(array?: T[]): T | undefined { - return array?.[array?.length - 1] -} - -export function stripFalsey(obj: T): Partial { - return Object.fromEntries(Object.entries(obj).filter(([, value]) => value)) as Partial -} diff --git a/plugin-server/src/config/config.ts b/plugin-server/src/config/config.ts index 7057d1818bd55..bb8ac06110198 100644 --- a/plugin-server/src/config/config.ts +++ b/plugin-server/src/config/config.ts @@ -176,14 +176,16 @@ export function getDefaultConfig(): PluginsServerConfig { SESSION_RECORDING_KAFKA_CONSUMPTION_STATISTICS_EVENT_INTERVAL_MS: 0, // 0 disables stats collection SESSION_RECORDING_KAFKA_FETCH_MIN_BYTES: 1_048_576, // 1MB // CDP - CDP_WATCHER_OBSERVATION_PERIOD: 10000, - CDP_WATCHER_DISABLED_PERIOD: 1000 * 60 * 10, - CDP_WATCHER_MAX_RECORDED_STATES: 10, - CDP_WATCHER_MAX_RECORDED_RATINGS: 10, - CDP_WATCHER_MAX_ALLOWED_TEMPORARY_DISABLED: 3, - CDP_WATCHER_MIN_OBSERVATIONS: 3, - CDP_WATCHER_OVERFLOW_RATING_THRESHOLD: 0.8, - CDP_WATCHER_DISABLED_RATING_THRESHOLD: 0.5, + CDP_WATCHER_COST_ERROR: 100, + CDP_WATCHER_COST_TIMING: 20, + CDP_WATCHER_COST_TIMING_LOWER_MS: 100, + CDP_WATCHER_COST_TIMING_UPPER_MS: 5000, + CDP_WATCHER_THRESHOLD_DEGRADED: 0.8, + CDP_WATCHER_BUCKET_SIZE: 10000, + CDP_WATCHER_DISABLED_TEMPORARY_TTL: 60 * 10, // 5 minutes + CDP_WATCHER_TTL: 60 * 60 * 24, // This is really long as it is essentially only important to make sure the key is eventually deleted + CDP_WATCHER_REFILL_RATE: 10, + CDP_WATCHER_DISABLED_TEMPORARY_MAX_COUNT: 3, CDP_ASYNC_FUNCTIONS_RUSTY_HOOK_TEAMS: '', } } diff --git a/plugin-server/src/main/pluginsServer.ts b/plugin-server/src/main/pluginsServer.ts index d12a2f4362fe1..0bcbf0e63597f 100644 --- a/plugin-server/src/main/pluginsServer.ts +++ b/plugin-server/src/main/pluginsServer.ts @@ -17,6 +17,7 @@ import { Hub, PluginServerCapabilities, PluginsServerConfig } from '../types' import { createHub, createKafkaClient, createKafkaProducerWrapper } from '../utils/db/hub' import { PostgresRouter } from '../utils/db/postgres' import { cancelAllScheduledJobs } from '../utils/node-schedule' +import { posthog } from '../utils/posthog' import { PubSub } from '../utils/pubsub' import { status } from '../utils/status' import { createRedisClient, delay } from '../utils/utils' @@ -165,6 +166,7 @@ export async function startPluginsServer( stopSessionRecordingBlobOverflowConsumer?.(), schedulerTasksConsumer?.disconnect(), ...shutdownCallbacks.map((cb) => cb()), + posthog.shutdownAsync(), ]) if (piscina) { diff --git a/plugin-server/src/types.ts b/plugin-server/src/types.ts index 92ec13670deed..2af0ef52b84b4 100644 --- a/plugin-server/src/types.ts +++ b/plugin-server/src/types.ts @@ -96,14 +96,16 @@ export const stringToPluginServerMode = Object.fromEntries( ) as Record export type CdpConfig = { - CDP_WATCHER_OBSERVATION_PERIOD: number - CDP_WATCHER_DISABLED_PERIOD: number - CDP_WATCHER_MAX_RECORDED_STATES: number - CDP_WATCHER_MAX_RECORDED_RATINGS: number - CDP_WATCHER_MAX_ALLOWED_TEMPORARY_DISABLED: number - CDP_WATCHER_MIN_OBSERVATIONS: number - CDP_WATCHER_OVERFLOW_RATING_THRESHOLD: number - CDP_WATCHER_DISABLED_RATING_THRESHOLD: number + CDP_WATCHER_COST_ERROR: number // The max cost of an erroring function + CDP_WATCHER_COST_TIMING: number // The max cost of a slow function + CDP_WATCHER_COST_TIMING_LOWER_MS: number // The lower bound in ms where the timing cost is not incurred + CDP_WATCHER_COST_TIMING_UPPER_MS: number // The upper bound in ms where the timing cost is fully incurred + CDP_WATCHER_THRESHOLD_DEGRADED: number // Percentage of the bucket where we count it as degraded + CDP_WATCHER_BUCKET_SIZE: number // The total bucket size + CDP_WATCHER_TTL: number // The expiry for the rate limit key + CDP_WATCHER_REFILL_RATE: number // The number of tokens to be refilled per second + CDP_WATCHER_DISABLED_TEMPORARY_TTL: number // How long a function should be temporarily disabled for + CDP_WATCHER_DISABLED_TEMPORARY_MAX_COUNT: number // How many times a function can be disabled before it is disabled permanently CDP_ASYNC_FUNCTIONS_RUSTY_HOOK_TEAMS: string } diff --git a/plugin-server/src/utils/posthog.ts b/plugin-server/src/utils/posthog.ts index b63604628eb2f..2de721b06ef4c 100644 --- a/plugin-server/src/utils/posthog.ts +++ b/plugin-server/src/utils/posthog.ts @@ -1,5 +1,7 @@ import { PostHog } from 'posthog-node' +import { Team } from '../types' + export const posthog = new PostHog('sTMFPsFhdP1Ssg', { host: 'https://us.i.posthog.com', }) @@ -7,3 +9,19 @@ export const posthog = new PostHog('sTMFPsFhdP1Ssg', { if (process.env.NODE_ENV === 'test') { posthog.disable() } + +export const captureTeamEvent = (team: Team, event: string, properties: Record = {}): void => { + posthog.capture({ + distinctId: team.uuid, + event, + properties: { + team: team.uuid, + ...properties, + }, + groups: { + project: team.uuid, + organization: team.organization_id, + instance: process.env.SITE_URL ?? 'unknown', + }, + }) +} diff --git a/plugin-server/src/worker/ingestion/group-type-manager.ts b/plugin-server/src/worker/ingestion/group-type-manager.ts index 21940b22d4d51..7263a2429ccca 100644 --- a/plugin-server/src/worker/ingestion/group-type-manager.ts +++ b/plugin-server/src/worker/ingestion/group-type-manager.ts @@ -1,7 +1,7 @@ import { GroupTypeIndex, GroupTypeToColumnIndex, Team, TeamId } from '../../types' import { PostgresRouter, PostgresUse } from '../../utils/db/postgres' import { timeoutGuard } from '../../utils/db/utils' -import { posthog } from '../../utils/posthog' +import { captureTeamEvent } from '../../utils/posthog' import { getByAge } from '../../utils/utils' import { TeamManager } from './team-manager' @@ -110,19 +110,6 @@ export class GroupTypeManager { return } - posthog.capture({ - distinctId: 'plugin-server', - event: 'group type ingested', - properties: { - team: team.uuid, - groupType, - groupTypeIndex, - }, - groups: { - project: team.uuid, - organization: team.organization_id, - instance: this.instanceSiteUrl, - }, - }) + captureTeamEvent(team, 'group type ingested', { groupType, groupTypeIndex }) } } diff --git a/plugin-server/tests/cdp/hog-watcher.test.ts b/plugin-server/tests/cdp/hog-watcher.test.ts new file mode 100644 index 0000000000000..2980a789f3683 --- /dev/null +++ b/plugin-server/tests/cdp/hog-watcher.test.ts @@ -0,0 +1,284 @@ +jest.mock('../../src/utils/now', () => { + return { + now: jest.fn(() => Date.now()), + } +}) +import { BASE_REDIS_KEY, HogWatcher, HogWatcherState } from '../../src/cdp/hog-watcher' +import { HogFunctionInvocationResult } from '../../src/cdp/types' +import { Hub } from '../../src/types' +import { createHub } from '../../src/utils/db/hub' +import { delay } from '../../src/utils/utils' +import { deleteKeysWithPrefix } from '../helpers/redis' + +const mockNow: jest.Mock = require('../../src/utils/now').now as any + +const createResult = (options: { + id: string + duration?: number + finished?: boolean + error?: string +}): HogFunctionInvocationResult => { + return { + invocation: { + id: 'invocation-id', + teamId: 2, + hogFunctionId: options.id, + globals: {} as any, + timings: [ + { + kind: 'async_function', + duration_ms: options.duration ?? 0, + }, + ], + }, + finished: options.finished ?? true, + error: options.error, + logs: [], + } +} + +describe('HogWatcher', () => { + describe('integration', () => { + let now: number + let hub: Hub + let closeHub: () => Promise + let watcher: HogWatcher + let mockStateChangeCallback: jest.Mock + + beforeEach(async () => { + ;[hub, closeHub] = await createHub() + + now = 1720000000000 + mockNow.mockReturnValue(now) + mockStateChangeCallback = jest.fn() + + await deleteKeysWithPrefix(hub.redisPool, BASE_REDIS_KEY) + + watcher = new HogWatcher(hub, mockStateChangeCallback) + }) + + const advanceTime = (ms: number) => { + now += ms + mockNow.mockReturnValue(now) + } + + afterEach(async () => { + jest.useRealTimers() + await closeHub() + jest.clearAllMocks() + }) + + it('should retrieve empty state', async () => { + const res = await watcher.getStates(['id1', 'id2']) + expect(res).toMatchInlineSnapshot(` + Object { + "id1": Object { + "rating": 1, + "state": 1, + "tokens": 10000, + }, + "id2": Object { + "rating": 1, + "state": 1, + "tokens": 10000, + }, + } + `) + }) + + const cases: [{ cost: number; state: number }, HogFunctionInvocationResult[]][] = [ + [{ cost: 0, state: 1 }, [createResult({ id: 'id1' })]], + [ + { cost: 0, state: 1 }, + [createResult({ id: 'id1' }), createResult({ id: 'id1' }), createResult({ id: 'id1' })], + ], + [ + { cost: 0, state: 1 }, + [ + createResult({ id: 'id1', duration: 10 }), + createResult({ id: 'id1', duration: 20 }), + createResult({ id: 'id1', duration: 100 }), + ], + ], + [ + { cost: 12, state: 1 }, + [ + createResult({ id: 'id1', duration: 1000 }), + createResult({ id: 'id1', duration: 1000 }), + createResult({ id: 'id1', duration: 1000 }), + ], + ], + [{ cost: 20, state: 1 }, [createResult({ id: 'id1', duration: 5000 })]], + [{ cost: 40, state: 1 }, [createResult({ id: 'id1', duration: 10000 })]], + [ + { cost: 141, state: 1 }, + [ + createResult({ id: 'id1', duration: 5000 }), + createResult({ id: 'id1', duration: 10000 }), + createResult({ id: 'id1', duration: 20000 }), + ], + ], + + [{ cost: 100, state: 1 }, [createResult({ id: 'id1', error: 'errored!' })]], + ] + + it.each(cases)('should update tokens based on results %s %s', async (expectedScore, results) => { + await watcher.observeResults(results) + const result = await watcher.getState('id1') + + expect(hub.CDP_WATCHER_BUCKET_SIZE - result.tokens).toEqual(expectedScore.cost) + expect(result.state).toEqual(expectedScore.state) + }) + + it('should max out scores', async () => { + let lotsOfResults = Array(10000).fill(createResult({ id: 'id1', error: 'error!' })) + + await watcher.observeResults(lotsOfResults) + + expect(await watcher.getState('id1')).toMatchInlineSnapshot(` + Object { + "rating": -0.0001, + "state": 3, + "tokens": -1, + } + `) + + lotsOfResults = Array(10000).fill(createResult({ id: 'id2' })) + + await watcher.observeResults(lotsOfResults) + + expect(await watcher.getState('id2')).toMatchInlineSnapshot(` + Object { + "rating": 1, + "state": 1, + "tokens": 10000, + } + `) + }) + + it('should refill over time', async () => { + hub.CDP_WATCHER_REFILL_RATE = 10 + await watcher.observeResults([ + createResult({ id: 'id1', duration: 10000 }), + createResult({ id: 'id1', duration: 10000 }), + createResult({ id: 'id1', duration: 10000 }), + ]) + + expect((await watcher.getState('id1')).tokens).toMatchInlineSnapshot(`9880`) + advanceTime(1000) + expect((await watcher.getState('id1')).tokens).toMatchInlineSnapshot(`9890`) + advanceTime(10000) + expect((await watcher.getState('id1')).tokens).toMatchInlineSnapshot(`9990`) + }) + + it('should remain disabled for period', async () => { + const badResults = Array(100).fill(createResult({ id: 'id1', error: 'error!' })) + + await watcher.observeResults(badResults) + + expect(mockStateChangeCallback).toHaveBeenCalledTimes(1) + expect(mockStateChangeCallback).toHaveBeenCalledWith('id1', HogWatcherState.disabledForPeriod) + + expect(await watcher.getState('id1')).toMatchInlineSnapshot(` + Object { + "rating": 0, + "state": 3, + "tokens": 0, + } + `) + + advanceTime(10000) + + // Should still be disabled even though tokens have been refilled + expect(await watcher.getState('id1')).toMatchInlineSnapshot(` + Object { + "rating": 0.01, + "state": 3, + "tokens": 100, + } + `) + }) + + describe('forceStateChange', () => { + it('should force healthy', async () => { + await watcher.forceStateChange('id1', HogWatcherState.healthy) + expect(await watcher.getState('id1')).toMatchInlineSnapshot(` + Object { + "rating": 1, + "state": 1, + "tokens": 10000, + } + `) + expect(mockStateChangeCallback).toHaveBeenCalledWith('id1', HogWatcherState.healthy) + }) + it('should force degraded', async () => { + await watcher.forceStateChange('id1', HogWatcherState.degraded) + expect(await watcher.getState('id1')).toMatchInlineSnapshot(` + Object { + "rating": 0.8, + "state": 1, + "tokens": 8000, + } + `) + expect(mockStateChangeCallback).toHaveBeenCalledWith('id1', HogWatcherState.degraded) + }) + it('should force disabledForPeriod', async () => { + await watcher.forceStateChange('id1', HogWatcherState.disabledForPeriod) + expect(await watcher.getState('id1')).toMatchInlineSnapshot(` + Object { + "rating": 0, + "state": 3, + "tokens": 0, + } + `) + expect(mockStateChangeCallback).toHaveBeenCalledWith('id1', HogWatcherState.disabledForPeriod) + }) + it('should force disabledIndefinitely', async () => { + await watcher.forceStateChange('id1', HogWatcherState.disabledIndefinitely) + expect(await watcher.getState('id1')).toMatchInlineSnapshot(` + Object { + "rating": 0, + "state": 4, + "tokens": 0, + } + `) + expect(mockStateChangeCallback).toHaveBeenCalledWith('id1', HogWatcherState.disabledIndefinitely) + }) + }) + + describe('disable logic', () => { + beforeEach(() => { + hub.CDP_WATCHER_BUCKET_SIZE = 100 + hub.CDP_WATCHER_DISABLED_TEMPORARY_TTL = 1 // Shorter ttl to help with testing + hub.CDP_WATCHER_DISABLED_TEMPORARY_MAX_COUNT = 3 + }) + + const reallyAdvanceTime = async (ms: number) => { + advanceTime(ms) + await delay(ms) + } + + it('count the number of times it has been disabled', async () => { + // Trigger the temporary disabled state 3 times + for (let i = 0; i < 2; i++) { + await watcher.observeResults([createResult({ id: 'id1', error: 'error!' })]) + expect((await watcher.getState('id1')).state).toEqual(HogWatcherState.disabledForPeriod) + await reallyAdvanceTime(1000) + expect((await watcher.getState('id1')).state).toEqual(HogWatcherState.degraded) + } + + expect(mockStateChangeCallback).toHaveBeenCalledTimes(2) + expect(mockStateChangeCallback.mock.calls[0]).toEqual(['id1', HogWatcherState.disabledForPeriod]) + expect(mockStateChangeCallback.mock.calls[1]).toEqual(['id1', HogWatcherState.disabledForPeriod]) + + await watcher.observeResults([createResult({ id: 'id1', error: 'error!' })]) + expect((await watcher.getState('id1')).state).toEqual(HogWatcherState.disabledIndefinitely) + await reallyAdvanceTime(1000) + expect((await watcher.getState('id1')).state).toEqual(HogWatcherState.disabledIndefinitely) + + expect(mockStateChangeCallback).toHaveBeenCalledTimes(3) + expect(mockStateChangeCallback.mock.calls[2]).toEqual(['id1', HogWatcherState.disabledIndefinitely]) + }) + }) + }) +}) diff --git a/plugin-server/tests/cdp/hog-watcher/hog-watcher-utils.test.ts b/plugin-server/tests/cdp/hog-watcher/hog-watcher-utils.test.ts deleted file mode 100644 index eb0215bbcfac5..0000000000000 --- a/plugin-server/tests/cdp/hog-watcher/hog-watcher-utils.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -jest.mock('../../../src/utils/now', () => { - return { - now: jest.fn(() => Date.now()), - } -}) - -import { - HogWatcherObservationPeriod, - HogWatcherRatingPeriod, - HogWatcherState, - HogWatcherStatePeriod, -} from '../../../src/cdp/hog-watcher/types' -import { calculateRating, deriveCurrentStateFromRatings, periodTimestamp } from '../../../src/cdp/hog-watcher/utils' -import { defaultConfig } from '../../../src/config/config' - -const config = defaultConfig - -describe('HogWatcher.utils', () => { - describe('calculateRating', () => { - // TODO: Change rating to account for numbers as well - low volume failures can still have a high rating as their impact is not so bad - const cases: Array<[Partial, number]> = [ - [{ successes: 9, failures: 1 }, 0.9], - [{ successes: 1, failures: 1 }, 0.5], - [{ successes: 0, failures: 1 }, 0], - [{ successes: 1, failures: 0 }, 1], - [{ asyncFunctionSuccesses: 9, asyncFunctionFailures: 1 }, 0.9], - [{ asyncFunctionSuccesses: 1, asyncFunctionFailures: 1 }, 0.5], - [{ asyncFunctionSuccesses: 0, asyncFunctionFailures: 1 }, 0], - [{ asyncFunctionSuccesses: 1, asyncFunctionFailures: 0 }, 1], - - // Mixed results - currently whichever is worse is the rating - [{ successes: 9, failures: 1, asyncFunctionSuccesses: 1, asyncFunctionFailures: 1 }, 0.5], - [{ successes: 1, failures: 1, asyncFunctionSuccesses: 9, asyncFunctionFailures: 1 }, 0.5], - [{ successes: 1, failures: 1, asyncFunctionSuccesses: 1, asyncFunctionFailures: 1 }, 0.5], - [{ successes: 0, failures: 0, asyncFunctionSuccesses: 9, asyncFunctionFailures: 1 }, 0.9], - ] - - it.each(cases)('should calculate the rating %s of %s', (vals, rating) => { - const observation: HogWatcherObservationPeriod = { - timestamp: Date.now(), - successes: 0, - failures: 0, - asyncFunctionFailures: 0, - asyncFunctionSuccesses: 0, - ...vals, - } - expect(calculateRating(observation)).toBe(rating) - }) - }) - - describe('deriveCurrentStateFromRatings', () => { - let now: number - let ratings: HogWatcherRatingPeriod[] - let states: HogWatcherStatePeriod[] - - beforeEach(() => { - now = periodTimestamp(config) - ratings = [] - states = [] - - jest.useFakeTimers() - jest.setSystemTime(now) - }) - - afterEach(() => { - jest.useRealTimers() - }) - - const advanceTime = (ms: number) => { - jest.advanceTimersByTime(ms) - } - - const updateState = (newRatings: number[], newStates: HogWatcherState[]) => { - for (let i = 0; i < Math.max(newRatings.length, newStates.length); i++) { - advanceTime(config.CDP_WATCHER_OBSERVATION_PERIOD) - - if (newStates[i]) { - states.push({ - timestamp: periodTimestamp(config), - state: newStates[i], - }) - } - - if (typeof newRatings[i] === 'number') { - ratings.push({ - timestamp: Date.now(), - rating: newRatings[i], - }) - } - } - } - - const currentState = () => deriveCurrentStateFromRatings(config, ratings, states) - const getAverageRating = () => - ratings.length ? ratings.reduce((acc, x) => acc + x.rating, 0) / ratings.length : 0 - - describe('1 - healthy', () => { - it('should be healthy with no ratings or previous states', () => { - expect(currentState()).toBe(HogWatcherState.healthy) - }) - - it.each(Object.values(HogWatcherState))( - 'should be whatever the last state is (%s) if no ratings', - (lastState) => { - updateState([], [lastState as any]) - expect(currentState()).toBe(lastState) - } - ) - - it('should not change if too few ratings', () => { - updateState([0, 0], []) - expect(getAverageRating()).toEqual(0) - expect(currentState()).toBe(HogWatcherState.healthy) - }) - - it('should move to overflow if enough ratings are unhealthy', () => { - updateState([1, 1, 0.8, 0.6, 0.6, 0.6, 0.6], []) - expect(states).toMatchObject([]) - expect(getAverageRating()).toBeLessThan(config.CDP_WATCHER_OVERFLOW_RATING_THRESHOLD) - expect(currentState()).toBe(HogWatcherState.overflowed) - }) - }) - - describe('2 - overflow', () => { - it('should stay in overflow if the rating does not change ', () => { - updateState([1, 1, 0.8, 0.6, 0.6, 0.6, 0.6], []) - expect(currentState()).toBe(HogWatcherState.overflowed) - expect(getAverageRating()).toBeLessThan(config.CDP_WATCHER_OVERFLOW_RATING_THRESHOLD) - expect(getAverageRating()).toBeGreaterThan(config.CDP_WATCHER_DISABLED_RATING_THRESHOLD) - - updateState([0.5, 0.5, 0.6, 0.7, 0.8, 1, 0.8], []) - expect(getAverageRating()).toBeLessThan(config.CDP_WATCHER_OVERFLOW_RATING_THRESHOLD) - expect(getAverageRating()).toBeGreaterThan(config.CDP_WATCHER_DISABLED_RATING_THRESHOLD) - expect(currentState()).toBe(HogWatcherState.overflowed) - }) - - it('should move back to healthy with enough healthy activity ', () => { - updateState([], [HogWatcherState.overflowed]) - expect(currentState()).toBe(HogWatcherState.overflowed) - updateState([0.5, 0.8, 0.9, 0.9, 1, 0.9, 1], []) - expect(getAverageRating()).toBeGreaterThan(config.CDP_WATCHER_OVERFLOW_RATING_THRESHOLD) - expect(currentState()).toBe(HogWatcherState.healthy) - }) - - it('should move to overflow if enough observations are unhealthy', () => { - updateState([1, 1, 0.8, 0.6, 0.6, 0.6, 0.6], []) - expect(states).toMatchObject([]) - expect(getAverageRating()).toBeLessThan(config.CDP_WATCHER_OVERFLOW_RATING_THRESHOLD) - expect(currentState()).toBe(HogWatcherState.overflowed) - }) - - it('should move to disabledForPeriod if sustained lower', () => { - updateState([0.5, 0.4, 0.4], []) - expect(currentState()).toBe(HogWatcherState.overflowed) - updateState([], [HogWatcherState.overflowed]) // Add the new state - expect(currentState()).toBe(HogWatcherState.overflowed) // Should still be the same - updateState([0.5, 0.4], []) // Add nearly enough ratings for next evaluation - expect(currentState()).toBe(HogWatcherState.overflowed) // Should still be the same - updateState([0.4], []) // One more rating and it can be evaluated - expect(getAverageRating()).toBeLessThan(config.CDP_WATCHER_DISABLED_RATING_THRESHOLD) - expect(currentState()).toBe(HogWatcherState.disabledForPeriod) - }) - - it('should go to disabledIndefinitely with enough bad states', () => { - updateState( - [], - [ - HogWatcherState.disabledForPeriod, - HogWatcherState.overflowed, - HogWatcherState.disabledForPeriod, - HogWatcherState.overflowed, - HogWatcherState.disabledForPeriod, - HogWatcherState.overflowed, - HogWatcherState.disabledForPeriod, - HogWatcherState.overflowed, - HogWatcherState.disabledForPeriod, - HogWatcherState.overflowed, - ] - ) - expect(currentState()).toBe(HogWatcherState.overflowed) - updateState([0.2, 0.2, 0.2, 0.2], []) - expect(currentState()).toBe(HogWatcherState.disabledIndefinitely) - }) - }) - - describe('3 - disabledForPeriod', () => { - it('should stay disabled for period until the period has passed ', () => { - updateState([], [HogWatcherState.disabledForPeriod]) - expect(currentState()).toBe(HogWatcherState.disabledForPeriod) - expect(states).toEqual([ - { state: HogWatcherState.disabledForPeriod, timestamp: periodTimestamp(config) }, - ]) - advanceTime(config.CDP_WATCHER_DISABLED_PERIOD - 1) - expect(currentState()).toBe(HogWatcherState.disabledForPeriod) - advanceTime(2) - expect(currentState()).toBe(HogWatcherState.overflowed) - }) - }) - - describe('4 - disabledIndefinitely', () => { - it('should stay in disabledIndefinitely no matter what', () => { - updateState([], [HogWatcherState.disabledIndefinitely]) - - expect(currentState()).toBe(HogWatcherState.disabledIndefinitely) - // Technically this wouldn't be possible but still good to test - updateState([1, 1, 1, 1, 1, 1, 1], []) - expect(currentState()).toBe(HogWatcherState.disabledIndefinitely) - }) - }) - }) -}) diff --git a/plugin-server/tests/cdp/hog-watcher/hog-watcher.test.ts b/plugin-server/tests/cdp/hog-watcher/hog-watcher.test.ts deleted file mode 100644 index a36d72794ce99..0000000000000 --- a/plugin-server/tests/cdp/hog-watcher/hog-watcher.test.ts +++ /dev/null @@ -1,442 +0,0 @@ -jest.mock('../../../src/utils/now', () => { - return { - now: jest.fn(() => Date.now()), - } -}) - -import { HogWatcher, HogWatcherActiveObservations } from '../../../src/cdp/hog-watcher/hog-watcher' -import { BASE_REDIS_KEY, runRedis } from '../../../src/cdp/hog-watcher/utils' -import { HogFunctionInvocationAsyncResponse, HogFunctionInvocationResult } from '../../../src/cdp/types' -import { defaultConfig } from '../../../src/config/config' -import { Hub } from '../../../src/types' -import { createHub } from '../../../src/utils/db/hub' -import { delay } from '../../../src/utils/utils' -import { deleteKeysWithPrefix } from '../../helpers/redis' - -const mockNow: jest.Mock = require('../../../src/utils/now').now as any - -const createResult = (id: string, finished = true, error?: string): HogFunctionInvocationResult => { - return { - invocation: { - id: 'invocation-id', - teamId: 2, - hogFunctionId: id, - globals: {} as any, - timings: [], - }, - finished, - error, - logs: [], - } -} - -const createAsyncResponse = (id: string, success = true): HogFunctionInvocationAsyncResponse => { - return { - state: '', - teamId: 2, - hogFunctionId: id, - asyncFunctionResponse: { - error: !success ? 'error' : null, - response: {}, - }, - } -} - -const config = defaultConfig - -describe('HogWatcher', () => { - describe('HogWatcherActiveObservations', () => { - let observer: HogWatcherActiveObservations - - beforeEach(() => { - observer = new HogWatcherActiveObservations(config) - jest.useFakeTimers() - jest.setSystemTime(1719229670000) - }) - - afterEach(() => { - jest.useRealTimers() - }) - - it('should update the observation', () => { - expect(observer.observations).toEqual({}) - - observer.observeResults([createResult('id1'), createResult('id1', false, 'error')]) - observer.observeAsyncFunctionResponses([createAsyncResponse('id1'), createAsyncResponse('id2', false)]) - - expect(observer.observations).toMatchInlineSnapshot(` - Object { - "id1": Object { - "asyncFunctionFailures": 0, - "asyncFunctionSuccesses": 1, - "failures": 1, - "successes": 1, - "timestamp": 1719229670000, - }, - "id2": Object { - "asyncFunctionFailures": 1, - "asyncFunctionSuccesses": 0, - "failures": 0, - "successes": 0, - "timestamp": 1719229670000, - }, - } - `) - - observer.observeAsyncFunctionResponses([createAsyncResponse('id2'), createAsyncResponse('id2')]) - - expect(observer.observations).toMatchInlineSnapshot(` - Object { - "id1": Object { - "asyncFunctionFailures": 0, - "asyncFunctionSuccesses": 1, - "failures": 1, - "successes": 1, - "timestamp": 1719229670000, - }, - "id2": Object { - "asyncFunctionFailures": 1, - "asyncFunctionSuccesses": 2, - "failures": 0, - "successes": 0, - "timestamp": 1719229670000, - }, - } - `) - }) - }) - - describe('integration', () => { - let now: number - let hub: Hub - let closeHub: () => Promise - - let watcher1: HogWatcher - let watcher2: HogWatcher - - const advanceTime = (ms: number) => { - now += ms - mockNow.mockReturnValue(now) - } - - beforeEach(async () => { - ;[hub, closeHub] = await createHub() - - now = 1720000000000 - mockNow.mockReturnValue(now) - - await deleteKeysWithPrefix(hub.redisPool, BASE_REDIS_KEY) - - watcher1 = new HogWatcher(hub) - watcher2 = new HogWatcher(hub) - await watcher1.start() - await watcher2.start() - }) - - afterEach(async () => { - await Promise.all([watcher1, watcher2].map((watcher) => watcher.stop())) - jest.useRealTimers() - await closeHub() - jest.clearAllMocks() - }) - - it('should retrieve empty state', async () => { - const res = await watcher1.fetchWatcher('id1') - expect(res).toEqual({ - ratings: [], - state: 1, - states: [], - }) - }) - - it('should store observations', () => { - watcher1.currentObservations.observeResults([createResult('id1'), createResult('id1', false, 'error')]) - watcher1.currentObservations.observeResults([createResult('id2'), createResult('id1')]) - watcher1.currentObservations.observeResults([createResult('id1')]) - - expect(watcher1.currentObservations.observations).toMatchObject({ - id1: { - failures: 1, - successes: 3, - timestamp: now, - }, - id2: { - failures: 0, - successes: 1, - timestamp: now, - }, - }) - - expect(watcher2.currentObservations.observations).toEqual({}) - }) - - it('should sync nothing if still in period', async () => { - watcher1.currentObservations.observeResults([createResult('id2'), createResult('id1')]) - expect(watcher1.currentObservations.observations).not.toEqual({}) - await watcher1.sync() - expect(watcher1.currentObservations.observations).not.toEqual({}) - expect(await watcher2.fetchState()).toEqual({ - observations: {}, - ratings: {}, - states: {}, - }) - }) - - it('should persist the in flight observations to redis', async () => { - watcher1.currentObservations.observeResults([createResult('id2'), createResult('id1')]) - advanceTime(config.CDP_WATCHER_OBSERVATION_PERIOD) - await watcher1.sync() - expect(watcher1.currentObservations.observations).toEqual({}) - const persistedState = await watcher2.fetchState() - expect(persistedState).toMatchObject({ - observations: { - id1: [ - { - failures: 0, - successes: 1, - timestamp: 1720000000000, - }, - ], - id2: [ - { - failures: 0, - successes: 1, - timestamp: 1720000000000, - }, - ], - }, - }) - }) - - it('should save the states and ratings to redis if enough periods passed', async () => { - watcher1.currentObservations.observeResults([createResult('id2'), createResult('id1')]) - watcher2.currentObservations.observeResults([ - createResult('id2', false, 'error'), - createResult('id1', true), - ]) - - let expectation: any = { - observations: { - id1: [expect.any(Object), expect.any(Object)], - id2: [expect.any(Object), expect.any(Object)], - }, - ratings: {}, - states: {}, - } - - // Move forward one period - this passes themasking period, ensuring that the observations are persisted - advanceTime(config.CDP_WATCHER_OBSERVATION_PERIOD) - await watcher1.sync() - await watcher2.sync() - await delay(100) // Allow pubsub to happen - expect(watcher2.states).toEqual({}) - // Watcher1 should be leader and have the globalState - expect(watcher1.globalState).toEqual(expectation) - expect(watcher2.globalState).toEqual(undefined) - expect(await watcher2.fetchState()).toEqual(expectation) - - // Move forward one final period and the initial observations should now be ratings - advanceTime(config.CDP_WATCHER_OBSERVATION_PERIOD) - await watcher1.sync() - await watcher2.sync() - await delay(100) // Allow pubsub to happen - - expectation = { - observations: {}, - ratings: { - id1: [{ rating: 1, timestamp: 1720000000000 }], - id2: [{ rating: 0.5, timestamp: 1720000000000 }], - }, - states: {}, - } - - expect(watcher2.states).toEqual({}) // No states yet as everything is healthy - expect(watcher1.globalState).toEqual(expectation) - // Persisted state should match the global state - expect(await watcher2.fetchState()).toEqual(expectation) - }) - - it('should move the function into a bad state after enough periods', async () => { - // We need to move N times forward to get past the masking period and have enough ratings to make a decision - // 2 for the persistance of the ratings, 3 more for the evaluation, 3 more for the subsequent evaluation - for (let i = 0; i < 2 + 3 + 3; i++) { - watcher1.currentObservations.observeResults([createResult('id1', false, 'error')]) - advanceTime(config.CDP_WATCHER_OBSERVATION_PERIOD) - await watcher1.sync() - } - await delay(100) - - expect(watcher1.globalState).toMatchObject({ - observations: {}, - ratings: { - id1: Array(7) - .fill(0) - .map((_, i) => ({ - rating: 0, - timestamp: 1720000000000 + i * config.CDP_WATCHER_OBSERVATION_PERIOD, - })), - }, - states: { - id1: [ - { - state: 2, - timestamp: 1720000040000, - }, - { - state: 3, - timestamp: 1720000080000, - }, - ], - }, - }) - - expect(watcher2.states['id1']).toEqual(3) - - advanceTime(config.CDP_WATCHER_DISABLED_PERIOD + 1) - await watcher1.sync() - await delay(100) - expect(watcher2.states['id1']).toEqual(2) - }) - - it('should save the states to redis so another watcher can grab it', async () => { - for (let i = 0; i < 5; i++) { - watcher1.currentObservations.observeResults([createResult('id1', false, 'error')]) - advanceTime(config.CDP_WATCHER_OBSERVATION_PERIOD) - await watcher1.sync() - } - await delay(100) - - expect(await watcher2.fetchWatcher('id1')).toMatchObject({ - state: 2, - states: [ - { - state: 2, - timestamp: 1720000040000, - }, - ], - }) - }) - - it('should load existing states from redis', async () => { - for (let i = 0; i < 5; i++) { - watcher1.currentObservations.observeResults([createResult('id1', false, 'error')]) - advanceTime(config.CDP_WATCHER_OBSERVATION_PERIOD) - await watcher1.sync() - } - - const newWatcher = new HogWatcher(hub) - await newWatcher.start() - expect(newWatcher.states).toEqual({ - id1: 2, - }) - }) - - it('should react to becoming or losing leader status', async () => { - watcher1.currentObservations.observeResults([createResult('id1', false, 'error')]) - advanceTime(config.CDP_WATCHER_OBSERVATION_PERIOD) - await watcher1.sync() - const stateExpectation = { - observations: { id1: [expect.any(Object)] }, - ratings: {}, - states: {}, - } - expect(watcher1.isLeader).toEqual(true) - expect(watcher1.globalState).toEqual(stateExpectation) - expect(watcher2.isLeader).toEqual(false) - expect(watcher2.globalState).toEqual(undefined) - - // Simulate the ttl running out - await runRedis(hub.redisPool, 'test', (client) => client.del(BASE_REDIS_KEY + '/leader')) - - // Watcher 2 goes first so will grab leadership - await Promise.all([watcher2.sync(), watcher1.sync()]) - expect(watcher1.isLeader).toEqual(false) - expect(watcher1.globalState).toEqual(undefined) - expect(watcher2.isLeader).toEqual(true) - expect(watcher2.globalState).toEqual(stateExpectation) - }) - - it('should move a problematic function in and out of overflow until eventually disabled', async () => { - // NOTE: The length here just happens be the right loop count to - - let maxLoops = 100 - while (watcher1.getFunctionState('id1') !== 4 && maxLoops > 0) { - maxLoops-- - if (watcher1.getFunctionState('id1') < 3) { - // If we are anything other than disables, simulate a bad invocations - watcher1.currentObservations.observeResults([createResult('id1', false, 'error')]) - advanceTime(config.CDP_WATCHER_OBSERVATION_PERIOD) - } else { - // Skip ahead if the function is disabled - advanceTime(config.CDP_WATCHER_DISABLED_PERIOD) - } - await watcher1.sync() - await delay(5) - } - - const states = watcher1.globalState?.states['id1'] ?? [] - const duration = Math.round((states[states.length - 1]!.timestamp - states[0]!.timestamp) / 1000 / 60) - // Little helper check to remind us the total time for a bad function to get to be permanently disabled - expect(`Time to fully disable: ${duration}mins`).toMatchInlineSnapshot(`"Time to fully disable: 63mins"`) - - expect(states).toMatchInlineSnapshot(` - Array [ - Object { - "state": 2, - "timestamp": 1720000040000, - }, - Object { - "state": 3, - "timestamp": 1720000080000, - }, - Object { - "state": 2, - "timestamp": 1720001280000, - }, - Object { - "state": 3, - "timestamp": 1720001320000, - }, - Object { - "state": 2, - "timestamp": 1720002520000, - }, - Object { - "state": 3, - "timestamp": 1720002560000, - }, - Object { - "state": 2, - "timestamp": 1720003760000, - }, - Object { - "state": 4, - "timestamp": 1720003800000, - }, - ] - `) - }) - - it('should react to incoming manual state changes', async () => { - await watcher1.forceStateChange('id1', 2) - await delay(100) - - const stateExpectation = { - observations: {}, - ratings: {}, - states: { - id1: [ - { - state: 2, - timestamp: 1720000000000, - }, - ], - }, - } - expect(watcher1.isLeader).toEqual(true) - expect(watcher1.globalState).toEqual(stateExpectation) - expect(watcher2.isLeader).toEqual(false) - expect(watcher2.globalState).toEqual(undefined) - }) - }) -}) diff --git a/plugin-server/tests/worker/ingestion/group-type-manager.test.ts b/plugin-server/tests/worker/ingestion/group-type-manager.test.ts index 46f2ada3086fc..3999d5a78cb93 100644 --- a/plugin-server/tests/worker/ingestion/group-type-manager.test.ts +++ b/plugin-server/tests/worker/ingestion/group-type-manager.test.ts @@ -1,15 +1,12 @@ import { Hub } from '../../../src/types' import { createHub } from '../../../src/utils/db/hub' -import { posthog } from '../../../src/utils/posthog' +import { captureTeamEvent } from '../../../src/utils/posthog' import { GroupTypeManager } from '../../../src/worker/ingestion/group-type-manager' import { resetTestDatabase } from '../../helpers/sql' jest.mock('../../../src/utils/status') jest.mock('../../../src/utils/posthog', () => ({ - posthog: { - identify: jest.fn(), - capture: jest.fn(), - }, + captureTeamEvent: jest.fn(), })) describe('GroupTypeManager()', () => { @@ -102,7 +99,7 @@ describe('GroupTypeManager()', () => { expect(hub.db.postgres.query).toHaveBeenCalledTimes(1) expect(groupTypeManager.insertGroupType).toHaveBeenCalledTimes(0) - expect(posthog.capture).not.toHaveBeenCalled() + expect(captureTeamEvent).not.toHaveBeenCalled() }) it('inserts value if it does not exist yet at next index, resets cache', async () => { @@ -117,19 +114,9 @@ describe('GroupTypeManager()', () => { expect(hub.db.postgres.query).toHaveBeenCalledTimes(3) // FETCH + INSERT + Team lookup const team = await hub.db.fetchTeam(2) - expect(posthog.capture).toHaveBeenCalledWith({ - distinctId: 'plugin-server', - event: 'group type ingested', - properties: { - team: team!.uuid, - groupType: 'second', - groupTypeIndex: 1, - }, - groups: { - project: team!.uuid, - organization: team!.organization_id, - instance: 'unknown', - }, + expect(captureTeamEvent).toHaveBeenCalledWith(team, 'group type ingested', { + groupType: 'second', + groupTypeIndex: 1, }) expect(await groupTypeManager.fetchGroupTypeIndex(2, 'third')).toEqual(2) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1030f89789153..ea51d354ce1bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,8 +221,8 @@ dependencies: specifier: ^3.1.5 version: 3.1.5(react@18.2.0) kea-forms: - specifier: ^3.1.3 - version: 3.1.3(kea@3.1.5) + specifier: ^3.2.0 + version: 3.2.0(kea@3.1.5) kea-loaders: specifier: ^3.0.0 version: 3.0.0(kea@3.1.5) @@ -272,8 +272,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0(postcss@8.4.31) posthog-js: - specifier: 1.154.5 - version: 1.154.5 + specifier: 1.154.6 + version: 1.154.6 posthog-js-lite: specifier: 3.0.0 version: 3.0.0 @@ -15332,8 +15332,8 @@ packages: resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} dev: false - /kea-forms@3.1.3(kea@3.1.5): - resolution: {integrity: sha512-110muG4hUz/4+hXZ2Wen+anrfS1GFoSRzB3I/83tFaKrcQmKEGvHiowUAKafwl/68cEhIHxyeEuUZYPpDH7uPw==} + /kea-forms@3.2.0(kea@3.1.5): + resolution: {integrity: sha512-R/ECGx6FxOZ2TEJv2GcFNLcQJePUK5Qd4Km81rN/7lBHd2hG4CAJglODVpcolWZ0RtcLcvMhHeTg/iqNz5pynw==} peerDependencies: kea: '>= 3.0.1' dependencies: @@ -17766,12 +17766,12 @@ packages: resolution: {integrity: sha512-dyajjnfzZD1tht4N7p7iwf7nBnR1MjVaVu+MKr+7gBgA39bn28wizCIJZztZPtHy4PY0YwtSGgwfBCuG/hnHgA==} dev: false - /posthog-js@1.154.5: - resolution: {integrity: sha512-YYhWckDIRObfCrQpiLq+fdcDTIbQp8ebiKi0ueGohMRgugIG9LJVSpBgCeCHZm2C7sOxDUNcAr3T5VBDUSQoOg==} + /posthog-js@1.154.6: + resolution: {integrity: sha512-8dA9xRex27cIbtSKwxNEvx+khGO+arlwfwyfD/TKuoc0TI5mFAXwtgxE45nnWFnP2hZ4CUCv2WMifLudIqJvcw==} dependencies: fflate: 0.4.8 preact: 10.23.1 - web-vitals: 4.2.2 + web-vitals: 4.2.3 dev: false /potpack@2.0.0: @@ -21668,8 +21668,8 @@ packages: '@zxing/text-encoding': 0.9.0 dev: true - /web-vitals@4.2.2: - resolution: {integrity: sha512-nYfoOqb4EmElljyXU2qdeE76KsvoHdftQKY4DzA9Aw8DervCg2bG634pHLrJ/d6+B4mE3nWTSJv8Mo7B2mbZkw==} + /web-vitals@4.2.3: + resolution: {integrity: sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q==} dev: false /webidl-conversions@3.0.1: diff --git a/posthog/api/debug_ch_queries.py b/posthog/api/debug_ch_queries.py index 5e1589156777d..fd323e2f95f08 100644 --- a/posthog/api/debug_ch_queries.py +++ b/posthog/api/debug_ch_queries.py @@ -1,3 +1,4 @@ +import json import re from typing import Optional @@ -33,17 +34,16 @@ def list(self, request): SELECT query_id, argMax(query, type) AS query, - argMax(query_json, type) AS query_json, argMax(query_start_time, type) AS query_start_time, argMax(exception, type) AS exception, argMax(query_duration_ms, type) AS query_duration_ms, argMax(ProfileEvents, type) as profile_events, + argMax(log_comment, type) AS log_comment, max(type) AS status FROM ( SELECT query_id, query, query_start_time, exception, query_duration_ms, toInt8(type) AS type, - JSONExtractRaw(log_comment, 'query') as query_json, - ProfileEvents + ProfileEvents, log_comment FROM clusterAllReplicas(%(cluster)s, system, query_log) WHERE query LIKE %(query)s AND @@ -63,18 +63,20 @@ def list(self, request): }, ) return Response( - [ - { - "query_id": resp[0], - "query": resp[1], - "queryJson": resp[2], - "timestamp": resp[3], - "exception": resp[4], - "execution_time": resp[5], - "profile_events": resp[6], - "status": resp[7], - "path": self._get_path(resp[1]), - } - for resp in response - ] + { + "queries": [ + { + "query_id": resp[0], + "query": resp[1], + "timestamp": resp[2], + "exception": resp[3], + "execution_time": resp[4], + "profile_events": resp[5], + "logComment": json.loads(resp[6]), + "status": resp[7], + "path": self._get_path(resp[1]), + } + for resp in response + ] + } ) diff --git a/posthog/api/hog_function.py b/posthog/api/hog_function.py index a9eddc7b1fae0..be9ee0fd475e2 100644 --- a/posthog/api/hog_function.py +++ b/posthog/api/hog_function.py @@ -31,8 +31,8 @@ class HogFunctionStatusSerializer(serializers.Serializer): state = serializers.ChoiceField(choices=[state.value for state in HogFunctionState]) - states: serializers.ListField = serializers.ListField(child=serializers.DictField()) - ratings: serializers.ListField = serializers.ListField(child=serializers.DictField()) + rating: serializers.FloatField = serializers.FloatField() + tokens: serializers.IntegerField = serializers.IntegerField() class HogFunctionMinimalSerializer(serializers.ModelSerializer): @@ -58,7 +58,7 @@ class Meta: class HogFunctionSerializer(HogFunctionMinimalSerializer): template = HogFunctionTemplateSerializer(read_only=True) - status = HogFunctionStatusSerializer(read_only=True) + status = HogFunctionStatusSerializer(read_only=True, required=False, allow_null=True) class Meta: model = HogFunction @@ -181,7 +181,7 @@ def update(self, instance: HogFunction, validated_data: dict, *args, **kwargs) - res: HogFunction = super().update(instance, validated_data) if res.enabled and res.status.get("state", 0) >= HogFunctionState.DISABLED_TEMPORARILY.value: - res.set_function_status(HogFunctionState.OVERFLOWED.value) + res.set_function_status(HogFunctionState.DEGRADED.value) return res diff --git a/posthog/api/organization.py b/posthog/api/organization.py index 5a66b6281e512..690fb6decea40 100644 --- a/posthog/api/organization.py +++ b/posthog/api/organization.py @@ -18,6 +18,7 @@ from posthog.models.organization import OrganizationMembership from posthog.models.signals import mute_selected_signals from posthog.models.team.util import delete_bulky_postgres_data +from posthog.models.uploaded_media import UploadedMedia from posthog.permissions import ( CREATE_METHODS, APIScopePermission, @@ -69,6 +70,9 @@ class OrganizationSerializer(serializers.ModelSerializer, UserPermissionsSeriali teams = serializers.SerializerMethodField() metadata = serializers.SerializerMethodField() member_count = serializers.SerializerMethodField() + logo_media_id = serializers.PrimaryKeyRelatedField( + queryset=UploadedMedia.objects.all(), required=False, allow_null=True + ) class Meta: model = Organization @@ -76,6 +80,7 @@ class Meta: "id", "name", "slug", + "logo_media_id", "created_at", "updated_at", "membership_level", @@ -111,7 +116,6 @@ def create(self, validated_data: dict, *args: Any, **kwargs: Any) -> Organizatio serializers.raise_errors_on_nested_writes("create", self, validated_data) user = self.context["request"].user organization, _, _ = Organization.objects.bootstrap(user, **validated_data) - return organization def get_membership_level(self, organization: Organization) -> Optional[OrganizationMembership.Level]: diff --git a/posthog/api/query.py b/posthog/api/query.py index f51ae9b3cbbaa..acda400e51b35 100644 --- a/posthog/api/query.py +++ b/posthog/api/query.py @@ -4,14 +4,13 @@ from django.http import JsonResponse from drf_spectacular.utils import OpenApiResponse from pydantic import BaseModel -from posthog.hogql_queries.query_runner import ExecutionMode, execution_mode_from_refresh +from rest_framework import status from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.exceptions import ValidationError, NotAuthenticated from rest_framework.request import Request from rest_framework.response import Response -from sentry_sdk import capture_exception -from rest_framework import status +from sentry_sdk import capture_exception, set_tag from posthog.api.documentation import extend_schema from posthog.api.mixins import PydanticModelMixin @@ -25,6 +24,7 @@ from posthog.errors import ExposedCHQueryError from posthog.hogql.ai import PromptUnclear, write_sql_from_prompt from posthog.hogql.errors import ExposedHogQLError +from posthog.hogql_queries.query_runner import ExecutionMode, execution_mode_from_refresh from posthog.models.user import User from posthog.rate_limit import ( AIBurstRateThrottle, @@ -159,3 +159,4 @@ def _tag_client_query_id(self, query_id: str | None): return tag_queries(client_query_id=query_id) + set_tag("client_query_id", query_id) diff --git a/posthog/api/shared.py b/posthog/api/shared.py index 97fb8a5cd288e..e37fe9de29297 100644 --- a/posthog/api/shared.py +++ b/posthog/api/shared.py @@ -86,7 +86,7 @@ class OrganizationBasicSerializer(serializers.ModelSerializer): class Meta: model = Organization - fields = ["id", "name", "slug", "membership_level"] + fields = ["id", "name", "slug", "logo_media_id", "membership_level"] def get_membership_level(self, organization: Organization) -> Optional[OrganizationMembership.Level]: membership = OrganizationMembership.objects.filter( diff --git a/posthog/api/test/__snapshots__/test_action.ambr b/posthog/api/test/__snapshots__/test_action.ambr index bd62b8e126df8..f5c0c2bdec046 100644 --- a/posthog/api/test/__snapshots__/test_action.ambr +++ b/posthog/api/test/__snapshots__/test_action.ambr @@ -186,6 +186,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -211,6 +212,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -368,6 +370,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -393,6 +396,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -568,6 +572,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -593,6 +598,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/__snapshots__/test_annotation.ambr b/posthog/api/test/__snapshots__/test_annotation.ambr index 85eda5a875c3f..77f85cdcaeecb 100644 --- a/posthog/api/test/__snapshots__/test_annotation.ambr +++ b/posthog/api/test/__snapshots__/test_annotation.ambr @@ -154,6 +154,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -276,6 +277,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -405,6 +407,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index 60610916f75ec..34374ff97c0f5 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -42,6 +42,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -184,6 +185,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -215,6 +217,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/__snapshots__/test_element.ambr b/posthog/api/test/__snapshots__/test_element.ambr index 88f9c67280e98..498ec7a5bcd92 100644 --- a/posthog/api/test/__snapshots__/test_element.ambr +++ b/posthog/api/test/__snapshots__/test_element.ambr @@ -105,6 +105,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index ab4768773c8df..c36d74b5acada 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -1820,6 +1820,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1845,6 +1846,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index 5e8efee8ca40a..70f4452c7f1fc 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -926,6 +926,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1040,6 +1041,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1166,6 +1168,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1191,6 +1194,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1445,6 +1449,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1560,6 +1565,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr index 8e1ac88a0efa8..d9d63f0ee948b 100644 --- a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr @@ -36,6 +36,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -158,6 +159,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1131,6 +1133,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -2042,6 +2045,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/__snapshots__/test_plugin.ambr b/posthog/api/test/__snapshots__/test_plugin.ambr index e424770da1794..7e35461d29c27 100644 --- a/posthog/api/test/__snapshots__/test_plugin.ambr +++ b/posthog/api/test/__snapshots__/test_plugin.ambr @@ -36,6 +36,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -61,6 +62,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -125,6 +127,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -190,6 +193,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -215,6 +219,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -249,6 +254,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -313,6 +319,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -346,6 +353,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -403,6 +411,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -428,6 +437,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -462,6 +472,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -526,6 +537,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -568,6 +580,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -639,6 +652,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -664,6 +678,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr index c5bfbdab0513b..23b0c43baa6bc 100644 --- a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr +++ b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr @@ -232,6 +232,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -329,6 +330,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -609,6 +611,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -707,6 +710,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1071,6 +1075,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1234,6 +1239,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1428,6 +1434,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1781,6 +1788,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1895,6 +1903,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -2014,6 +2023,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -2039,6 +2049,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -2144,6 +2155,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -3845,6 +3857,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -4063,6 +4076,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -4160,6 +4174,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -4539,6 +4554,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -4903,6 +4919,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -5098,6 +5115,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -5203,6 +5221,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -5705,6 +5724,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -6585,6 +6605,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -8751,6 +8772,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -10119,6 +10141,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -10402,6 +10425,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -10624,6 +10648,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -11189,6 +11214,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -11406,6 +11432,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -12020,6 +12047,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -12244,6 +12272,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -12671,6 +12700,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -12768,6 +12798,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -13201,6 +13232,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -13598,6 +13630,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -13634,6 +13667,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -13698,6 +13732,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -14076,6 +14111,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -14101,6 +14137,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -14206,6 +14243,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -14354,6 +14392,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr b/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr index d01ca325c40c2..01e71135160a8 100644 --- a/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr +++ b/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr @@ -186,6 +186,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -337,6 +338,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -456,6 +458,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/api/test/test_hog_function.py b/posthog/api/test/test_hog_function.py index 57f7290cd5fc5..77bfdfd965e46 100644 --- a/posthog/api/test/test_hog_function.py +++ b/posthog/api/test/test_hog_function.py @@ -6,7 +6,7 @@ from posthog.constants import AvailableFeature from posthog.models.action.action import Action -from posthog.models.hog_functions.hog_function import HogFunction +from posthog.models.hog_functions.hog_function import DEFAULT_STATE, HogFunction from posthog.test.base import APIBaseTest, ClickhouseTestMixin, QueryMatchingTest from posthog.cdp.templates.webhook.template_webhook import template as template_webhook from posthog.cdp.templates.slack.template_slack import template as template_slack @@ -167,7 +167,7 @@ def test_create_hog_function(self, *args): "filters": {"bytecode": ["_h", 29]}, "icon_url": None, "template": None, - "status": {"ratings": [], "state": 0, "states": []}, + "status": {"rating": 0, "state": 0, "tokens": 0}, } @patch("posthog.permissions.posthoganalytics.feature_enabled", return_value=True) @@ -484,7 +484,7 @@ def test_generates_filters_bytecode(self, *args): def test_loads_status_when_enabled_and_available(self, *args): with patch("posthog.plugins.plugin_server_api.requests.get") as mock_get: mock_get.return_value.status_code = status.HTTP_200_OK - mock_get.return_value.json.return_value = {"state": 1, "states": [], "ratings": []} + mock_get.return_value.json.return_value = {"state": 1, "tokens": 0, "rating": 0} response = self.client.post( f"/api/projects/{self.team.id}/hog_functions/", @@ -499,7 +499,7 @@ def test_loads_status_when_enabled_and_available(self, *args): assert response.status_code == status.HTTP_201_CREATED, response.json() response = self.client.get(f"/api/projects/{self.team.id}/hog_functions/{response.json()['id']}") - assert response.json()["status"] == {"state": 1, "states": [], "ratings": []} + assert response.json()["status"] == {"state": 1, "tokens": 0, "rating": 0} @patch("posthog.permissions.posthoganalytics.feature_enabled", return_value=True) def test_does_not_crash_when_status_not_available(self, *args): @@ -519,14 +519,14 @@ def test_does_not_crash_when_status_not_available(self, *args): ) assert response.status_code == status.HTTP_201_CREATED, response.json() response = self.client.get(f"/api/projects/{self.team.id}/hog_functions/{response.json()['id']}") - assert response.json()["status"] == {"ratings": [], "state": 0, "states": []} + assert response.json()["status"] == DEFAULT_STATE @patch("posthog.permissions.posthoganalytics.feature_enabled", return_value=True) def test_patches_status_on_enabled_update(self, *args): with patch("posthog.plugins.plugin_server_api.requests.get") as mock_get: with patch("posthog.plugins.plugin_server_api.requests.patch") as mock_patch: mock_get.return_value.status_code = status.HTTP_200_OK - mock_get.return_value.json.return_value = {"state": 4, "states": [], "ratings": []} + mock_get.return_value.json.return_value = {"state": 4, "tokens": 0, "rating": 0} response = self.client.post( f"/api/projects/{self.team.id}/hog_functions/", diff --git a/posthog/api/test/test_user.py b/posthog/api/test/test_user.py index 781f8dfeb5292..e44c12e9f4623 100644 --- a/posthog/api/test/test_user.py +++ b/posthog/api/test/test_user.py @@ -95,12 +95,14 @@ def test_retrieve_current_user(self): "id": str(self.organization.id), "name": self.organization.name, "slug": slugify(self.organization.name), + "logo_media_id": None, "membership_level": 1, }, { "id": str(self.new_org.id), "name": "New Organization", "slug": "new-organization", + "logo_media_id": None, "membership_level": 1, }, ], diff --git a/posthog/hogql_queries/query_runner.py b/posthog/hogql_queries/query_runner.py index 4bb0832acff36..faa75c20b128c 100644 --- a/posthog/hogql_queries/query_runner.py +++ b/posthog/hogql_queries/query_runner.py @@ -6,7 +6,7 @@ import structlog from prometheus_client import Counter from pydantic import BaseModel, ConfigDict -from sentry_sdk import capture_exception, push_scope +from sentry_sdk import capture_exception, push_scope, set_tag, get_traceparent from posthog.caching.utils import is_stale, ThresholdMode, cache_target_age, last_refresh_from_cached_result from posthog.clickhouse.client.execute_async import enqueue_process_query_task, get_query_status, QueryNotFoundError @@ -538,7 +538,17 @@ def run( dashboard_id: Optional[int] = None, ) -> CR | CacheMissResponse | QueryStatusResponse: cache_key = self.get_cache_key() + tag_queries(cache_key=cache_key) + tag_queries(sentry_trace=get_traceparent()) + set_tag("cache_key", cache_key) + if insight_id: + tag_queries(insight_id=insight_id) + set_tag("insight_id", str(insight_id)) + if dashboard_id: + tag_queries(dashboard_id=dashboard_id) + set_tag("dashboard_id", str(dashboard_id)) + self.query_id = query_id or self.query_id CachedResponse: type[CR] = self.cached_response_type cache_manager = QueryCacheManager( diff --git a/posthog/migrations/0452_organization_logo.py b/posthog/migrations/0452_organization_logo.py new file mode 100644 index 0000000000000..b1ca34e0bf586 --- /dev/null +++ b/posthog/migrations/0452_organization_logo.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.14 on 2024-07-22 08:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + atomic = False # Added to support concurrent index creation + dependencies = [ + ("posthog", "0451_datawarehousetable_updated_at_and_more"), + ] + + operations = [ + # Using the approach with CREATE INDEX CONCURRENTLY from 0415_pluginconfig_match_action + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AddField( + model_name="organization", + name="logo_media", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="posthog.uploadedmedia" + ), + ), + ], + database_operations=[ + # We add -- existing-table-constraint-ignore to ignore the constraint validation in CI. + migrations.RunSQL( + """ + ALTER TABLE "posthog_organization" ADD COLUMN "logo_media_id" uuid NULL CONSTRAINT "posthog_organization_logo_media_id_1c12c9dc_fk_posthog_u" REFERENCES "posthog_uploadedmedia"("id") DEFERRABLE INITIALLY DEFERRED; -- existing-table-constraint-ignore + SET CONSTRAINTS "posthog_organization_logo_media_id_1c12c9dc_fk_posthog_u" IMMEDIATE; -- existing-table-constraint-ignore + """, + reverse_sql=""" + ALTER TABLE "posthog_organization" DROP COLUMN IF EXISTS "logo_media_id"; + """, + ), + # We add CONCURRENTLY to the create command + migrations.RunSQL( + """ + CREATE INDEX CONCURRENTLY "posthog_organization_logo_media_id_1c12c9dc" ON "posthog_organization" ("logo_media_id"); + """, + reverse_sql=""" + DROP INDEX IF EXISTS "posthog_organization_logo_media_id_1c12c9dc"; + """, + ), + ], + ), + ] diff --git a/posthog/models/hog_functions/hog_function.py b/posthog/models/hog_functions/hog_function.py index 5d5de9ec73960..45151dcad6986 100644 --- a/posthog/models/hog_functions/hog_function.py +++ b/posthog/models/hog_functions/hog_function.py @@ -16,11 +16,8 @@ reload_hog_functions_on_workers, ) -DEFAULT_STATE = { - "state": 0, - "ratings": [], - "states": [], -} +DEFAULT_STATE = {"state": 0, "tokens": 0, "rating": 0} + logger = structlog.get_logger(__name__) @@ -28,7 +25,7 @@ class HogFunctionState(enum.Enum): UNKNOWN = 0 HEALTHY = 1 - OVERFLOWED = 2 + DEGRADED = 2 DISABLED_TEMPORARILY = 3 DISABLED_PERMANENTLY = 4 diff --git a/posthog/models/organization.py b/posthog/models/organization.py index a64d4d91d1f12..5f146fc8b45a2 100644 --- a/posthog/models/organization.py +++ b/posthog/models/organization.py @@ -120,6 +120,9 @@ class PluginsAccessLevel(models.IntegerChoices): ) name: models.CharField = models.CharField(max_length=64) slug: LowercaseSlugField = LowercaseSlugField(unique=True, max_length=MAX_SLUG_LENGTH) + logo_media: models.ForeignKey = models.ForeignKey( + "posthog.UploadedMedia", on_delete=models.SET_NULL, null=True, blank=True + ) created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True) updated_at: models.DateTimeField = models.DateTimeField(auto_now=True) plugins_access_level: models.PositiveSmallIntegerField = models.PositiveSmallIntegerField( diff --git a/posthog/session_recordings/queries/session_recording_list_from_filters.py b/posthog/session_recordings/queries/session_recording_list_from_filters.py index 24d16e0f2b415..b7b928f10690f 100644 --- a/posthog/session_recordings/queries/session_recording_list_from_filters.py +++ b/posthog/session_recordings/queries/session_recording_list_from_filters.py @@ -265,6 +265,7 @@ def _having_predicates(self) -> ast.And | Constant: op = ( ast.CompareOperationOp.GtEq if self._filter.recording_duration_filter.operator == "gt" + or self._filter.recording_duration_filter.operator == "gte" else ast.CompareOperationOp.LtEq ) exprs.append( diff --git a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr index 16bc71a7219e4..a1f05dcef0d2c 100644 --- a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr +++ b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr @@ -413,6 +413,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -438,6 +439,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1761,6 +1763,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -1786,6 +1789,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -2280,6 +2284,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -2305,6 +2310,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -2888,6 +2894,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -2913,6 +2920,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -3459,6 +3467,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -3490,6 +3499,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -3515,6 +3525,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -3774,6 +3785,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -3963,6 +3975,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -4261,6 +4274,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -4380,6 +4394,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -4405,6 +4420,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -4893,6 +4909,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -4918,6 +4935,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -5483,6 +5501,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -5508,6 +5527,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -6018,6 +6038,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -6043,6 +6064,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -6595,6 +6617,7 @@ "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -6620,6 +6643,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr index 2af373f63ba61..1f6210fb4d933 100644 --- a/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr +++ b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr @@ -49,6 +49,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", @@ -309,6 +310,7 @@ SELECT "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", + "posthog_organization"."logo_media_id", "posthog_organization"."created_at", "posthog_organization"."updated_at", "posthog_organization"."plugins_access_level", diff --git a/posthog/temporal/data_imports/pipelines/sql_database/__init__.py b/posthog/temporal/data_imports/pipelines/sql_database/__init__.py index a3524f378e1a7..d5902480986f1 100644 --- a/posthog/temporal/data_imports/pipelines/sql_database/__init__.py +++ b/posthog/temporal/data_imports/pipelines/sql_database/__init__.py @@ -201,7 +201,7 @@ def get_column_hints(engine: Engine, schema_name: str, table_name: str) -> dict[ columns[column_name] = { "data_type": "decimal", "precision": numeric_precision or 76, - "scale": numeric_scale or 16, + "scale": numeric_scale or 32, } return columns diff --git a/posthog/temporal/data_imports/pipelines/sql_database/test/test_sql_database.py b/posthog/temporal/data_imports/pipelines/sql_database/test/test_sql_database.py index d604d1e38c35b..edf217c4a67a4 100644 --- a/posthog/temporal/data_imports/pipelines/sql_database/test/test_sql_database.py +++ b/posthog/temporal/data_imports/pipelines/sql_database/test/test_sql_database.py @@ -35,7 +35,7 @@ def test_get_column_hints_numeric_with_missing_scale_and_precision(): mock_engine = _setup([("column", "numeric", None, None)]) assert get_column_hints(mock_engine, "some_schema", "some_table") == { - "column": {"data_type": "decimal", "precision": 76, "scale": 16} + "column": {"data_type": "decimal", "precision": 76, "scale": 32} } diff --git a/vector/replay-capture/Dockerfile b/vector/replay-capture/Dockerfile new file mode 100644 index 0000000000000..e32ab326aadd7 --- /dev/null +++ b/vector/replay-capture/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine as config-builder + +RUN apk add -U yq + +WORKDIR /config +COPY vector.yaml . +# evaluate with yq, basically to expand anchors (which vector doesn't support) +RUN yq -i e 'explode(.)' vector.yaml + +FROM timberio/vector:0.40.X-alpine + +COPY --from=config-builder /config/vector.yaml /etc/vector/vector.yaml diff --git a/vector/replay-capture/tests.yaml b/vector/replay-capture/tests.yaml new file mode 100644 index 0000000000000..1c30b243c480c --- /dev/null +++ b/vector/replay-capture/tests.yaml @@ -0,0 +1,76 @@ +tests: + - name: Basic Test + inputs: + - insert_at: quota_check + type: vrl + source: | + .message = [{}] + .message[0].properties = {} + .message[0].properties."$$session_id" = "123" + .message[0].properties."$$window_id" = "123" + .message[0].properties."token" = "123" + .message[0].properties."distinct_id" = "123" + .message[0].properties."$$snapshot_data" = [{"offset": 123}] + + .ip = "0.0.0.0" + .timestamp = now() + ."_" = "123456789" + %token = "123" + + outputs: + - conditions: + - source: | + assert!(is_string(.uuid)) + assert!(is_string(%headers.token)) + assert!(is_string(parse_json!(.data).uuid)) + assert!(parse_json!(.data).properties."$$snapshot_items"[0].offset == 123) + type: vrl + extract_from: overflow_check._unmatched + - name: Quota limited + inputs: + - insert_at: quota_check + type: vrl + source: | + .message = [{}] + .message[0].properties = {} + .message[0].properties."$$session_id" = "123" + .message[0].properties."$$window_id" = "123" + .message[0].properties."token" = "limited_token" + .message[0].properties."distinct_id" = "123" + .message[0].properties."$$snapshot_data" = [{"offset": 123}] + + .ip = "0.0.0.0" + .timestamp = now() + ."_" = "123456789" + %token = "limited_token" + + outputs: + - conditions: + - source: | + true + type: vrl + extract_from: metric_quota_dropped + - name: Overflow + inputs: + - insert_at: quota_check + type: vrl + source: | + .message = [{}] + .message[0].properties = {} + .message[0].properties."$$session_id" = "overflow_session" + .message[0].properties."$$window_id" = "123" + .message[0].properties."token" = "123" + .message[0].properties."distinct_id" = "123" + .message[0].properties."$$snapshot_data" = [{"offset": 123}] + + .ip = "0.0.0.0" + .timestamp = now() + ."_" = "123456789" + %token = "123" + + outputs: + - conditions: + - source: | + true + type: vrl + extract_from: overflow_check.overflow diff --git a/vector/replay-capture/tests/overflow_sessions.csv b/vector/replay-capture/tests/overflow_sessions.csv new file mode 100644 index 0000000000000..0d825115e2944 --- /dev/null +++ b/vector/replay-capture/tests/overflow_sessions.csv @@ -0,0 +1,2 @@ +session_id +overflow_session \ No newline at end of file diff --git a/vector/replay-capture/tests/quota_limited_teams.csv b/vector/replay-capture/tests/quota_limited_teams.csv new file mode 100644 index 0000000000000..cc5b54d345ddd --- /dev/null +++ b/vector/replay-capture/tests/quota_limited_teams.csv @@ -0,0 +1,2 @@ +token +limited_token \ No newline at end of file diff --git a/vector/replay-capture/vector.yaml b/vector/replay-capture/vector.yaml new file mode 100644 index 0000000000000..8bcc51a10676f --- /dev/null +++ b/vector/replay-capture/vector.yaml @@ -0,0 +1,221 @@ +acknowledgements: + enabled: true + +api: + enabled: true + address: 0.0.0.0:8686 + playground: true + +enrichment_tables: + quota_limited_teams: + type: file + file: + path: '${QUOTA_LIMITED_TEAMS_PATH:-/etc/vector/data/quota_limited_teams.csv}' + encoding: + type: csv + schema: + token: string + overflow_sessions: + type: file + file: + path: '${OVERFLOW_SESSIONS_PATH:-/etc/vector/data/overflow_sessions.csv}' + encoding: + type: csv + schema: + token: string + +sources: + capture_server: + type: http_server + address: 0.0.0.0:8000 + strict_path: false + query_parameters: + - _ + host_key: ip + decoding: + codec: vrl + vrl: + source: | + ._message, err = decode_gzip(.message) + if err == null { + .message = parse_json!(del(._message)) + } else { + # if we failed to decode gzip then ._message is empty + .message = parse_json!(.message) + del(._message) + } + .message[0].distinct_id = .message[0]."$$distinct_id" || .message[0].properties.distinct_id || .message[0].distinct_id + if is_integer(.message[0].distinct_id) { + .message[0].distinct_id, _ = to_string(.message[0].distinct_id) + } + + %token = .message[0].properties.token || .message[0].api_key + + if !is_string(%token) || !is_string(.message[0].distinct_id) { + for_each(array(.message)) -> |_index, value| { + del(value.properties."$$snapshot_data") + } + log(truncate(encode_json(.), 1000), rate_limit_secs: 0) + } + + assert!(is_string(.message[0].distinct_id), "distinct_id is required") + assert!(is_string(.message[0].properties."$$session_id"), "$$session_id is required") + assert!(is_string(%token), "token is required") + +transforms: + quota_check: + type: route + inputs: + - capture_server + route: + quota_limited: + type: vrl + source: | + _, err = get_enrichment_table_record("quota_limited_teams", { "token": %token }) + err == null # err is not null if row not found, we want to drop where the row _is_ found + + events_parsed: + type: remap + inputs: + - quota_check._unmatched + drop_on_abort: true + drop_on_error: true + reroute_dropped: true + source: | + event = { + "ip": .ip, + "uuid": uuid_v7(), + "distinct_id": .message[0].distinct_id, + "session_id": .message[0].properties."$$session_id", + "now": format_timestamp!(.timestamp, "%+", "UTC"), + "token": %token, + } + + event.sent_at, err = from_unix_timestamp(to_int!(."_"), "milliseconds") + if err != null { + event.sent_at = event.now + } else { + event.sent_at = format_timestamp!(event.sent_at, "%+", "UTC") + } + + snapshot_items = flatten(map_values(array!(.message)) -> |value| { + if is_array(value.properties."$$snapshot_data") { + array!(value.properties."$$snapshot_data") + } else { + [value.properties."$$snapshot_data"] + } + }) + + data = { + "uuid": event.uuid, + "event": "$$snapshot_items", + "properties": { + "distinct_id": event.distinct_id, + "$$session_id": .message[0].properties."$$session_id", + "$$window_id": .message[0].properties."$$window_id", + "$$snapshot_source": .message[0].properties."$$snapshot_source" || "web", + "$$snapshot_items": snapshot_items + } + } + + event.data = encode_json(data) + . = event + + %headers = { + "token": .token + } + + overflow_check: + type: route + inputs: + - events_parsed + route: + overflow: + type: vrl + source: | + _, err = get_enrichment_table_record("overflow_sessions", { "session_id": .session_id }) + err == null # err is not null if row not found, we want to drop where the row _is_ found + + log_errors: + type: remap + inputs: + - events_parsed.dropped + source: | + log({ + "event": "error", + "reason": "events_parsed.dropped", + "token": %token, + "session_id": .message[0].properties."$$session_id", + "distinct_id": .message[0].distinct_id + }, rate_limit_secs: 0) + + metric_quota_dropped: + type: log_to_metric + inputs: + - quota_check.quota_limited + metrics: + - type: counter + field: message + kind: incremental + name: vector_capture_quota_dropped_count + tags: + token: '{{%token}}' +sinks: + # invalid sink to catch and raise errors + # without this vector drops them silently + error: + type: file + path: '' + encoding: + codec: json + acknowledgements: + enabled: true + inputs: + - log_errors + + dropped: + type: blackhole + acknowledgements: + enabled: true + inputs: + - metric_quota_dropped + + kafka: &kafka + type: kafka + acknowledgements: + enabled: true + inputs: + - overflow_check._unmatched + buffer: + - type: memory + max_events: 10000 + when_full: block + bootstrap_servers: $KAFKA_BOOSTRAP_SERVERS + topic: $KAFKA_EVENTS_TOPIC + compression: gzip + key_field: .session_id + headers_key: '%headers' + tls: + enabled: false + encoding: + codec: json + librdkafka_options: + client.id: ${CLIENT_ID:-$HOSTNAME} + linger.ms: '0' + topic.metadata.refresh.interval.ms: '20000' + queue.buffering.max.kbytes: '1048576' + queue.buffering.max.messages: '100' + message.max.bytes: '64000000' + batch.size: '1600000' + batch.num.messages: '100' + sticky.partitioning.linger.ms: '25' + enable.idempotence: 'false' + max.in.flight.requests.per.connection: '1000000' + partitioner: 'consistent_random' + message_timeout_ms: 10000 + socket_timeout_ms: 5000 + kafka_overflow: + <<: *kafka + inputs: + - overflow_check.overflow + topic: $KAFKA_OVERFLOW_TOPIC