diff --git a/.eslintrc.js b/.eslintrc.js index 7989f4d9c471e..3d730b9f777bc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -147,6 +147,10 @@ module.exports = { element: 'a', message: 'use instead', }, + { + element: 'ReactMarkdown', + message: 'use instead', + }, ], }, ], @@ -175,9 +179,9 @@ module.exports = { message: 'use instead', }, { - element:'MonacoEditor', + element: 'MonacoEditor', message: 'use instead', - } + }, ], }, ], diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml index f00214da5d20a..5538f237dfca1 100644 --- a/.github/workflows/ci-e2e.yml +++ b/.github/workflows/ci-e2e.yml @@ -65,7 +65,7 @@ jobs: - name: Group spec files into chunks of three id: chunk - run: echo "chunks=$(ls cypress/e2e/* | jq --slurp --raw-input -c 'split("\n")[:-1] | _nwise(3) | join("\n")' | jq --slurp -c .)" >> $GITHUB_OUTPUT + run: echo "chunks=$(ls cypress/e2e/* | jq --slurp --raw-input -c 'split("\n")[:-1] | _nwise(2) | join("\n")' | jq --slurp -c .)" >> $GITHUB_OUTPUT container: name: Build and cache container image diff --git a/.github/workflows/storybook-chromatic.yml b/.github/workflows/storybook-chromatic.yml index 0ad36ae9ebf9f..19b153aa0bfda 100644 --- a/.github/workflows/storybook-chromatic.yml +++ b/.github/workflows/storybook-chromatic.yml @@ -165,8 +165,24 @@ jobs: if [ $ADDED -gt 0 ] || [ $MODIFIED -gt 0 ]; then echo "Snapshots updated ($ADDED new, $MODIFIED changed), running OptiPNG" apt update && apt install -y optipng - git add frontend/__snapshots__/ playwright/ - pnpm lint-staged + optipng -clobber -o4 -strip all + + # we don't want to _always_ run OptiPNG + # so, we run it after checking for a diff + # but, the files we diffed might then be changed by OptiPNG + # and as a result they might no longer be different... + + # we check again + git diff --name-status frontend/__snapshots__/ # For debugging + ADDED=$(git diff --name-status frontend/__snapshots__/ | grep '^A' | wc -l) + MODIFIED=$(git diff --name-status frontend/__snapshots__/ | grep '^M' | wc -l) + DELETED=$(git diff --name-status frontend/__snapshots__/ | grep '^D' | wc -l) + TOTAL=$(git diff --name-status frontend/__snapshots__/ | wc -l) + + if [ $ADDED -gt 0 ] || [ $MODIFIED -gt 0 ]; then + echo "Snapshots updated ($ADDED new, $MODIFIED changed), _even after_ running OptiPNG" + git add frontend/__snapshots__/ playwright/ + fi fi echo "${{ matrix.browser }}-${{ matrix.shard }}-added=$ADDED" >> $GITHUB_OUTPUT diff --git a/.husky/pre-commit b/.husky/pre-commit index fdf550a219f53..fab6428a1a72a 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,16 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -# Check if staged files contain any added or modified PNGs - skip when merging -if \ - git rev-parse -q --verify MERGE_HEAD \ - && git diff --cached --name-status | grep '^[AM]' | grep -q '.png$' -then - # Error if OptiPNG is not installed - if ! command -v optipng >/dev/null; then - echo "PNG files must be optimized before being committed, but OptiPNG is not installed! Fix this with \`brew/apt install optipng\`." - exit 1 - fi -fi - pnpm lint-staged diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 0cda6703cfd37..9b0a76da1d367 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -15,6 +15,17 @@ const setupMsw = () => { // Make sure the msw worker is started worker.start({ quiet: true, + onUnhandledRequest(request, print) { + // MSW warns on all unhandled requests, but we don't necessarily care + const pathAllowList = ['/images/'] + + if (pathAllowList.some((path) => request.url.pathname.startsWith(path))) { + return + } + + // Otherwise, default MSW warning behavior + print.warning() + }, }) ;(window as any).__mockServiceWorker = worker ;(window as any).POSTHOG_APP_CONTEXT = getStorybookAppContext() diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index 190543bd22b81..ed720a36e1ba5 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -72,7 +72,7 @@ module.exports = { const storyContext = (await getStoryContext(page, context)) as StoryContext const { skip = false, snapshotBrowsers = ['chromium'] } = storyContext.parameters?.testOptions ?? {} - browserContext.setDefaultTimeout(3000) // Reduce the default timeout from 30 s to 3 s to pre-empt Jest timeouts + browserContext.setDefaultTimeout(5000) // Reduce the default timeout from 30 s to 5 s to pre-empt Jest timeouts if (!skip) { const currentBrowser = browserContext.browser()!.browserType().name() as SupportedBrowserName if (snapshotBrowsers.includes(currentBrowser)) { @@ -208,7 +208,8 @@ async function expectLocatorToMatchStorySnapshot( // Compare structural similarity instead of raw pixels - reducing false positives // See https://github.com/americanexpress/jest-image-snapshot#recommendations-when-using-ssim-comparison comparisonMethod: 'ssim', - failureThreshold: 0.0003, + // 0.01 would be a 1% difference + failureThreshold: 0.01, failureThresholdType: 'percent', }) } diff --git a/bin/plugin-server b/bin/plugin-server index 75000245ac5da..157e7896fc90b 100755 --- a/bin/plugin-server +++ b/bin/plugin-server @@ -46,7 +46,15 @@ if [ $? -ne 0 ]; then exit 1 fi -[[ -n $DEBUG ]] && cmd="pnpm start:dev" || cmd="node dist/index.js" +if [[ -n $DEBUG ]]; then + if [[ -n $NO_WATCH ]]; then + cmd="pnpm start:devNoWatch" + else + cmd="pnpm start:dev" + fi +else + cmd="node dist/index.js" +fi if [[ -n $NO_RESTART_LOOP ]]; then echo "▶️ Starting plugin server..." diff --git a/cypress/e2e/auth-password-reset.cy.ts b/cypress/e2e/auth-password-reset.cy.ts new file mode 100644 index 0000000000000..8669141a52c4f --- /dev/null +++ b/cypress/e2e/auth-password-reset.cy.ts @@ -0,0 +1,54 @@ +describe('Password Reset', () => { + beforeEach(() => { + cy.get('[data-attr=top-menu-toggle]').click() + cy.get('[data-attr=top-menu-item-logout]').click() + cy.location('pathname').should('eq', '/login') + }) + + it('Can request password reset', () => { + cy.get('[data-attr=login-email]').type('fake@posthog.com').should('have.value', 'fake@posthog.com').blur() + cy.get('[data-attr=forgot-password]', { timeout: 5000 }).should('be.visible') // Wait for login precheck (note blur above) + cy.get('[data-attr="forgot-password"]').click() + cy.location('pathname').should('eq', '/reset') + cy.get('[data-attr="reset-email"]').type('test@posthog.com') + cy.get('button[type=submit]').click() + cy.get('div').should('contain', 'Request received successfully!') + cy.get('b').should('contain', 'test@posthog.com') + }) + + it('Cannot reset with invalid token', () => { + cy.visit('/reset/user_id/token') + cy.get('div').should('contain', 'The provided link is invalid or has expired. ') + }) + + it('Shows validation error if passwords do not match', () => { + cy.visit('/reset/e2e_test_user/e2e_test_token') + cy.get('[data-attr="password"]').type('12345678') + cy.get('.ant-progress-bg').should('be.visible') + cy.get('[data-attr="password-confirm"]').type('1234567A') + cy.get('button[type=submit]').click() + cy.get('.text-danger').should('contain', 'Passwords do not match') + cy.location('pathname').should('eq', '/reset/e2e_test_user/e2e_test_token') // not going anywhere + }) + + it('Shows validation error if password is too short', () => { + cy.visit('/reset/e2e_test_user/e2e_test_token') + cy.get('[data-attr="password"]').type('123') + cy.get('[data-attr="password-confirm"]').type('123') + cy.get('button[type=submit]').click() + cy.get('.text-danger').should('be.visible') + cy.get('.text-danger').should('contain', 'must be at least 8 characters') + cy.location('pathname').should('eq', '/reset/e2e_test_user/e2e_test_token') // not going anywhere + }) + + it('Can reset password with valid token', () => { + cy.visit('/reset/e2e_test_user/e2e_test_token') + cy.get('[data-attr="password"]').type('NEW123456789') + cy.get('[data-attr="password-confirm"]').type('NEW123456789') + cy.get('button[type=submit]').click() + cy.get('.Toastify__toast--success').should('be.visible') + + // assert the user was redirected; can't test actual redirection to /insights because the test handler doesn't actually log in the user + cy.location('pathname').should('not.contain', '/reset/e2e_test_user/e2e_test_token') + }) +}) diff --git a/cypress/e2e/auth.cy.ts b/cypress/e2e/auth.cy.ts index dae569410a12e..e837f83a5b9f7 100644 --- a/cypress/e2e/auth.cy.ts +++ b/cypress/e2e/auth.cy.ts @@ -87,58 +87,3 @@ describe('Auth', () => { cy.location('pathname').should('eq', urls.projectHomepage()) }) }) - -describe('Password Reset', () => { - beforeEach(() => { - cy.get('[data-attr=top-menu-toggle]').click() - cy.get('[data-attr=top-menu-item-logout]').click() - cy.location('pathname').should('eq', '/login') - }) - - it('Can request password reset', () => { - cy.get('[data-attr=login-email]').type('fake@posthog.com').should('have.value', 'fake@posthog.com').blur() - cy.get('[data-attr=forgot-password]', { timeout: 5000 }).should('be.visible') // Wait for login precheck (note blur above) - cy.get('[data-attr="forgot-password"]').click() - cy.location('pathname').should('eq', '/reset') - cy.get('[data-attr="reset-email"]').type('test@posthog.com') - cy.get('button[type=submit]').click() - cy.get('div').should('contain', 'Request received successfully!') - cy.get('b').should('contain', 'test@posthog.com') - }) - - it('Cannot reset with invalid token', () => { - cy.visit('/reset/user_id/token') - cy.get('div').should('contain', 'The provided link is invalid or has expired. ') - }) - - it('Shows validation error if passwords do not match', () => { - cy.visit('/reset/e2e_test_user/e2e_test_token') - cy.get('[data-attr="password"]').type('12345678') - cy.get('.ant-progress-bg').should('be.visible') - cy.get('[data-attr="password-confirm"]').type('1234567A') - cy.get('button[type=submit]').click() - cy.get('.text-danger').should('contain', 'Passwords do not match') - cy.location('pathname').should('eq', '/reset/e2e_test_user/e2e_test_token') // not going anywhere - }) - - it('Shows validation error if password is too short', () => { - cy.visit('/reset/e2e_test_user/e2e_test_token') - cy.get('[data-attr="password"]').type('123') - cy.get('[data-attr="password-confirm"]').type('123') - cy.get('button[type=submit]').click() - cy.get('.text-danger').should('be.visible') - cy.get('.text-danger').should('contain', 'must be at least 8 characters') - cy.location('pathname').should('eq', '/reset/e2e_test_user/e2e_test_token') // not going anywhere - }) - - it('Can reset password with valid token', () => { - cy.visit('/reset/e2e_test_user/e2e_test_token') - cy.get('[data-attr="password"]').type('NEW123456789') - cy.get('[data-attr="password-confirm"]').type('NEW123456789') - cy.get('button[type=submit]').click() - cy.get('.Toastify__toast--success').should('be.visible') - - // assert the user was redirected; can't test actual redirection to /insights because the test handler doesn't actually log in the user - cy.location('pathname').should('not.contain', '/reset/e2e_test_user/e2e_test_token') - }) -}) diff --git a/cypress/e2e/dashboard-deletion.ts b/cypress/e2e/dashboard-deletion.ts new file mode 100644 index 0000000000000..42f2194b9f0c2 --- /dev/null +++ b/cypress/e2e/dashboard-deletion.ts @@ -0,0 +1,61 @@ +import { urls } from 'scenes/urls' +import { randomString } from '../support/random' +import { dashboard, dashboards, insight, savedInsights } from '../productAnalytics' + +describe('deleting dashboards', () => { + it('can delete dashboard without deleting the insights', () => { + cy.visit(urls.savedInsights()) // get insights list into turbo mode + cy.clickNavMenu('dashboards') + + const dashboardName = randomString('dashboard-') + const insightName = randomString('insight-') + + dashboards.createAndGoToEmptyDashboard(dashboardName) + dashboard.addInsightToEmptyDashboard(insightName) + + cy.get('[data-attr="dashboard-three-dots-options-menu"]').click() + cy.get('button').contains('Delete dashboard').click() + cy.get('[data-attr="dashboard-delete-submit"]').click() + + savedInsights.checkInsightIsInListView(insightName) + }) + + // TODO: this test works locally, just not in CI + it.skip('can delete dashboard and delete the insights', () => { + cy.visit(urls.savedInsights()) // get insights list into turbo mode + cy.clickNavMenu('dashboards') + + const dashboardName = randomString('dashboard-') + const dashboardToKeepName = randomString('dashboard-to-keep') + const insightName = randomString('insight-') + const insightToKeepName = randomString('insight-to-keep-') + + dashboards.createAndGoToEmptyDashboard(dashboardName) + dashboard.addInsightToEmptyDashboard(insightName) + + cy.clickNavMenu('dashboards') + + dashboards.createAndGoToEmptyDashboard(dashboardToKeepName) + dashboard.addInsightToEmptyDashboard(insightToKeepName) + + cy.visit(urls.savedInsights()) + cy.wait('@loadInsightList').then(() => { + cy.get('.saved-insights tr a').should('be.visible') + + // load the named insight + cy.contains('.saved-insights tr', insightToKeepName).within(() => { + cy.get('.row-name a').click() + }) + + insight.addInsightToDashboard(dashboardName, { visitAfterAdding: true }) + + cy.get('[data-attr="dashboard-three-dots-options-menu"]').click() + cy.get('button').contains('Delete dashboard').click() + cy.contains('span.LemonCheckbox', "Delete this dashboard's insights").click() + cy.get('[data-attr="dashboard-delete-submit"]').click() + + savedInsights.checkInsightIsInListView(insightToKeepName) + savedInsights.checkInsightIsNotInListView(insightName) + }) + }) +}) diff --git a/cypress/e2e/dashboard-duplication.ts b/cypress/e2e/dashboard-duplication.ts new file mode 100644 index 0000000000000..0be744d46980f --- /dev/null +++ b/cypress/e2e/dashboard-duplication.ts @@ -0,0 +1,98 @@ +import { randomString } from '../support/random' +import { urls } from 'scenes/urls' +import { dashboard, dashboards, duplicateDashboardFromMenu, savedInsights } from '../productAnalytics' + +describe('duplicating dashboards', () => { + let dashboardName, insightName, expectedCopiedDashboardName, expectedCopiedInsightName + + beforeEach(() => { + dashboardName = randomString('dashboard-') + expectedCopiedDashboardName = `${dashboardName} (Copy)` + + insightName = randomString('insight-') + expectedCopiedInsightName = `${insightName} (Copy)` + + cy.visit(urls.savedInsights()) // get insights list into turbo mode + cy.clickNavMenu('dashboards') + + dashboards.createAndGoToEmptyDashboard(dashboardName) + dashboard.addInsightToEmptyDashboard(insightName) + + cy.contains('h4', insightName).click() // get insight into turbo mode + }) + + describe('from the dashboard list', () => { + it('can duplicate a dashboard without duplicating insights', () => { + cy.clickNavMenu('dashboards') + cy.get('[placeholder="Search for dashboards"]').type(dashboardName) + + cy.contains('[data-attr="dashboards-table"] tr', dashboardName).within(() => { + cy.get('[data-attr="more-button"]').click() + }) + duplicateDashboardFromMenu() + cy.get('h1.page-title').should('have.text', expectedCopiedDashboardName) + + cy.wait('@createDashboard').then(() => { + cy.get('.CardMeta h4').should('have.text', insightName).should('not.have.text', '(Copy)') + cy.contains('h4', insightName).click() + // this works when actually using the site, but not in Cypress + // cy.get('[data-attr="save-to-dashboard-button"] .LemonBadge').should('have.text', '2') + }) + }) + + it('can duplicate a dashboard and duplicate insights', () => { + cy.clickNavMenu('dashboards') + cy.get('[placeholder="Search for dashboards"]').type(dashboardName) + + cy.contains('[data-attr="dashboards-table"] tr', dashboardName).within(() => { + cy.get('[data-attr="more-button"]').click() + }) + duplicateDashboardFromMenu(true) + cy.get('h1.page-title').should('have.text', expectedCopiedDashboardName) + + cy.wait('@createDashboard').then(() => { + cy.contains('h4', expectedCopiedInsightName).click() + cy.get('[data-attr="save-to-dashboard-button"] .LemonBadge').should('have.text', '1') + }) + + savedInsights.checkInsightIsInListView(insightName) + savedInsights.checkInsightIsInListView(expectedCopiedInsightName) + }) + }) + + describe('from the dashboard', () => { + it('can duplicate a dashboard without duplicating insights', () => { + cy.clickNavMenu('dashboards') + dashboards.visitDashboard(dashboardName) + + cy.get('[data-attr="dashboard-three-dots-options-menu"]').click() + duplicateDashboardFromMenu() + cy.get('h1.page-title').should('have.text', expectedCopiedDashboardName) + + cy.wait('@createDashboard').then(() => { + cy.get('.CardMeta h4').should('have.text', insightName).should('not.have.text', '(Copy)') + cy.contains('h4', insightName).click() + // this works when actually using the site, but not in Cypress + // cy.get('[data-attr="save-to-dashboard-button"] .LemonBadge').should('have.text', '2') + }) + savedInsights.checkInsightIsInListView(insightName) + savedInsights.checkInsightIsNotInListView(expectedCopiedInsightName) + }) + it('can duplicate a dashboard and duplicate insights', () => { + cy.clickNavMenu('dashboards') + dashboards.visitDashboard(dashboardName) + + cy.get('[data-attr="dashboard-three-dots-options-menu"]').click() + duplicateDashboardFromMenu(true) + cy.get('h1.page-title').should('have.text', expectedCopiedDashboardName) + + cy.wait('@createDashboard').then(() => { + cy.contains('h4', expectedCopiedInsightName).click() + cy.get('[data-attr="save-to-dashboard-button"] .LemonBadge').should('have.text', '1') + }) + + savedInsights.checkInsightIsInListView(insightName) + savedInsights.checkInsightIsInListView(expectedCopiedInsightName) + }) + }) +}) diff --git a/cypress/e2e/dashboard.cy.ts b/cypress/e2e/dashboard.cy.ts index fcf0b6f066db2..b40466727c716 100644 --- a/cypress/e2e/dashboard.cy.ts +++ b/cypress/e2e/dashboard.cy.ts @@ -1,6 +1,5 @@ -import { urls } from 'scenes/urls' import { randomString } from '../support/random' -import { insight, savedInsights, dashboards, dashboard, duplicateDashboardFromMenu } from '../productAnalytics' +import { insight, dashboards, dashboard } from '../productAnalytics' describe('Dashboard', () => { beforeEach(() => { @@ -211,157 +210,4 @@ describe('Dashboard', () => { cy.wait(200) cy.get('.page-title').contains(dashboardName).should('exist') }) - - describe('duplicating dashboards', () => { - let dashboardName, insightName, expectedCopiedDashboardName, expectedCopiedInsightName - - beforeEach(() => { - dashboardName = randomString('dashboard-') - expectedCopiedDashboardName = `${dashboardName} (Copy)` - - insightName = randomString('insight-') - expectedCopiedInsightName = `${insightName} (Copy)` - - cy.visit(urls.savedInsights()) // get insights list into turbo mode - cy.clickNavMenu('dashboards') - - dashboards.createAndGoToEmptyDashboard(dashboardName) - dashboard.addInsightToEmptyDashboard(insightName) - - cy.contains('h4', insightName).click() // get insight into turbo mode - }) - - describe('from the dashboard list', () => { - it('can duplicate a dashboard without duplicating insights', () => { - cy.clickNavMenu('dashboards') - cy.get('[placeholder="Search for dashboards"]').type(dashboardName) - - cy.contains('[data-attr="dashboards-table"] tr', dashboardName).within(() => { - cy.get('[data-attr="more-button"]').click() - }) - duplicateDashboardFromMenu() - cy.get('h1.page-title').should('have.text', expectedCopiedDashboardName) - - cy.wait('@createDashboard').then(() => { - cy.get('.CardMeta h4').should('have.text', insightName).should('not.have.text', '(Copy)') - cy.contains('h4', insightName).click() - // this works when actually using the site, but not in Cypress - // cy.get('[data-attr="save-to-dashboard-button"] .LemonBadge').should('have.text', '2') - }) - }) - - it('can duplicate a dashboard and duplicate insights', () => { - cy.clickNavMenu('dashboards') - cy.get('[placeholder="Search for dashboards"]').type(dashboardName) - - cy.contains('[data-attr="dashboards-table"] tr', dashboardName).within(() => { - cy.get('[data-attr="more-button"]').click() - }) - duplicateDashboardFromMenu(true) - cy.get('h1.page-title').should('have.text', expectedCopiedDashboardName) - - cy.wait('@createDashboard').then(() => { - cy.contains('h4', expectedCopiedInsightName).click() - cy.get('[data-attr="save-to-dashboard-button"] .LemonBadge').should('have.text', '1') - }) - - savedInsights.checkInsightIsInListView(insightName) - savedInsights.checkInsightIsInListView(expectedCopiedInsightName) - }) - }) - - describe('from the dashboard', () => { - it('can duplicate a dashboard without duplicating insights', () => { - cy.clickNavMenu('dashboards') - dashboards.visitDashboard(dashboardName) - - cy.get('[data-attr="dashboard-three-dots-options-menu"]').click() - duplicateDashboardFromMenu() - cy.get('h1.page-title').should('have.text', expectedCopiedDashboardName) - - cy.wait('@createDashboard').then(() => { - cy.get('.CardMeta h4').should('have.text', insightName).should('not.have.text', '(Copy)') - cy.contains('h4', insightName).click() - // this works when actually using the site, but not in Cypress - // cy.get('[data-attr="save-to-dashboard-button"] .LemonBadge').should('have.text', '2') - }) - savedInsights.checkInsightIsInListView(insightName) - savedInsights.checkInsightIsNotInListView(expectedCopiedInsightName) - }) - it('can duplicate a dashboard and duplicate insights', () => { - cy.clickNavMenu('dashboards') - dashboards.visitDashboard(dashboardName) - - cy.get('[data-attr="dashboard-three-dots-options-menu"]').click() - duplicateDashboardFromMenu(true) - cy.get('h1.page-title').should('have.text', expectedCopiedDashboardName) - - cy.wait('@createDashboard').then(() => { - cy.contains('h4', expectedCopiedInsightName).click() - cy.get('[data-attr="save-to-dashboard-button"] .LemonBadge').should('have.text', '1') - }) - - savedInsights.checkInsightIsInListView(insightName) - savedInsights.checkInsightIsInListView(expectedCopiedInsightName) - }) - }) - }) - - describe('deleting dashboards', () => { - it('can delete dashboard without deleting the insights', () => { - cy.visit(urls.savedInsights()) // get insights list into turbo mode - cy.clickNavMenu('dashboards') - - const dashboardName = randomString('dashboard-') - const insightName = randomString('insight-') - - dashboards.createAndGoToEmptyDashboard(dashboardName) - dashboard.addInsightToEmptyDashboard(insightName) - - cy.get('[data-attr="dashboard-three-dots-options-menu"]').click() - cy.get('button').contains('Delete dashboard').click() - cy.get('[data-attr="dashboard-delete-submit"]').click() - - savedInsights.checkInsightIsInListView(insightName) - }) - - // TODO: this test works locally, just not in CI - it.skip('can delete dashboard and delete the insights', () => { - cy.visit(urls.savedInsights()) // get insights list into turbo mode - cy.clickNavMenu('dashboards') - - const dashboardName = randomString('dashboard-') - const dashboardToKeepName = randomString('dashboard-to-keep') - const insightName = randomString('insight-') - const insightToKeepName = randomString('insight-to-keep-') - - dashboards.createAndGoToEmptyDashboard(dashboardName) - dashboard.addInsightToEmptyDashboard(insightName) - - cy.clickNavMenu('dashboards') - - dashboards.createAndGoToEmptyDashboard(dashboardToKeepName) - dashboard.addInsightToEmptyDashboard(insightToKeepName) - - cy.visit(urls.savedInsights()) - cy.wait('@loadInsightList').then(() => { - cy.get('.saved-insights tr a').should('be.visible') - - // load the named insight - cy.contains('.saved-insights tr', insightToKeepName).within(() => { - cy.get('.row-name a').click() - }) - - insight.addInsightToDashboard(dashboardName, { visitAfterAdding: true }) - - cy.get('[data-attr="dashboard-three-dots-options-menu"]').click() - cy.get('button').contains('Delete dashboard').click() - cy.contains('span.LemonCheckbox', "Delete this dashboard's insights").click() - cy.get('[data-attr="dashboard-delete-submit"]').click() - - savedInsights.checkInsightIsInListView(insightToKeepName) - savedInsights.checkInsightIsNotInListView(insightName) - }) - }) - }) }) diff --git a/cypress/e2e/insights-date-picker.ts b/cypress/e2e/insights-date-picker.ts new file mode 100644 index 0000000000000..cdf3bb0beca5f --- /dev/null +++ b/cypress/e2e/insights-date-picker.ts @@ -0,0 +1,18 @@ +describe('insights date picker', () => { + it('Can set the date filter and show the right grouping interval', () => { + cy.get('[data-attr=date-filter]').click() + cy.get('div').contains('Yesterday').should('exist').click() + cy.get('[data-attr=interval-filter]').should('contain', 'Hour') + }) + + it('Can set a custom rolling date range', () => { + cy.get('[data-attr=date-filter]').click() + cy.get('[data-attr=rolling-date-range-input]').type('{selectall}5{enter}') + cy.get('[data-attr=rolling-date-range-date-options-selector]').click() + cy.get('.RollingDateRangeFilter__popover > div').contains('days').should('exist').click() + cy.get('.RollingDateRangeFilter__label').should('contain', 'In the last').click() + + // Test that the button shows the correct formatted range + cy.get('[data-attr=date-filter]').get('span').contains('Last 5 days').should('exist') + }) +}) diff --git a/cypress/e2e/insights-navigation-open-directly.cy.ts b/cypress/e2e/insights-navigation-open-directly.cy.ts new file mode 100644 index 0000000000000..406445d9f6636 --- /dev/null +++ b/cypress/e2e/insights-navigation-open-directly.cy.ts @@ -0,0 +1,71 @@ +import { urls } from 'scenes/urls' +import { decideResponse } from '../fixtures/api/decide' +import { insight } from '../productAnalytics' + +const hogQLQuery = `select event, + count() + from events + group by event, + properties.$browser, + person.properties.email + order by count() desc + limit 2` + +// For tests related to trends please check trendsElements.js +describe('Insights', () => { + beforeEach(() => { + cy.intercept('https://app.posthog.com/decide/*', (req) => + req.reply( + decideResponse({ + hogql: true, + 'data-exploration-insights': true, + }) + ) + ) + + cy.visit(urls.insightNew()) + }) + + describe('navigation', () => { + describe('opening a new insight directly', () => { + it('can open a new trends insight', () => { + insight.newInsight('TRENDS') + cy.get('.trends-insights-container canvas').should('exist') + cy.get('tr').should('have.length.gte', 2) + }) + + it('can open a new funnels insight', () => { + insight.newInsight('FUNNELS') + cy.get('.funnels-empty-state__title').should('exist') + }) + + it.skip('can open a new retention insight', () => { + insight.newInsight('RETENTION') + cy.get('.RetentionContainer canvas').should('exist') + cy.get('.RetentionTable__Tab').should('have.length', 66) + }) + + it('can open a new paths insight', () => { + insight.newInsight('PATHS') + cy.get('.Paths g').should('have.length.gte', 5) // not a fixed value unfortunately + }) + + it('can open a new stickiness insight', () => { + insight.newInsight('STICKINESS') + cy.get('.trends-insights-container canvas').should('exist') + }) + + it('can open a new lifecycle insight', () => { + insight.newInsight('LIFECYCLE') + cy.get('.trends-insights-container canvas').should('exist') + }) + + it('can open a new SQL insight', () => { + insight.newInsight('SQL') + insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') + cy.get('[data-attr="hogql-query-editor"]').should('exist') + cy.get('tr.DataTable__row').should('have.length.gte', 2) + }) + }) + }) +}) diff --git a/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts b/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts new file mode 100644 index 0000000000000..ca286d2e7c765 --- /dev/null +++ b/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts @@ -0,0 +1,84 @@ +import { urls } from 'scenes/urls' +import { decideResponse } from '../fixtures/api/decide' +import { insight } from '../productAnalytics' + +const hogQLQuery = `select event, + count() + from events + group by event, + properties.$browser, + person.properties.email + order by count() desc + limit 2` + +// For tests related to trends please check trendsElements.js +describe('Insights', () => { + beforeEach(() => { + cy.intercept('https://app.posthog.com/decide/*', (req) => + req.reply( + decideResponse({ + hogql: true, + 'data-exploration-insights': true, + }) + ) + ) + + cy.visit(urls.insightNew()) + }) + + describe('navigation', () => { + describe('opening a new insight after opening a new SQL insight', () => { + // TRICKY: these tests have identical assertions to the ones above, but we need to open a SQL insight first + // and then click a different tab to switch to that insight. + // this is because we had a bug where doing that would mean after starting to load the new insight, + // the SQL insight would be unexpectedly re-selected and the page would switch back to it + + beforeEach(() => { + insight.newInsight('SQL') + insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') + cy.get('[data-attr="hogql-query-editor"]').should('exist') + cy.get('tr.DataTable__row').should('have.length.gte', 2) + }) + + it('can open a new trends insight', () => { + insight.clickTab('TRENDS') + cy.get('.trends-insights-container canvas').should('exist') + cy.get('tr').should('have.length.gte', 2) + cy.contains('tr', 'No insight results').should('not.exist') + }) + + it('can open a new funnels insight', () => { + insight.clickTab('FUNNELS') + cy.get('.funnels-empty-state__title').should('exist') + }) + + it('can open a new retention insight', () => { + insight.clickTab('RETENTION') + cy.get('.RetentionContainer canvas').should('exist') + cy.get('.RetentionTable__Tab').should('have.length', 66) + }) + + it('can open a new paths insight', () => { + insight.clickTab('PATH') + cy.get('.Paths g').should('have.length.gte', 5) // not a fixed value unfortunately + }) + + it('can open a new stickiness insight', () => { + insight.clickTab('STICKINESS') + cy.get('.trends-insights-container canvas').should('exist') + }) + + it('can open a new lifecycle insight', () => { + insight.clickTab('LIFECYCLE') + cy.get('.trends-insights-container canvas').should('exist') + }) + + it('can open a new SQL insight', () => { + insight.clickTab('SQL') + insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') + cy.get('[data-attr="hogql-query-editor"]').should('exist') + cy.get('tr.DataTable__row').should('have.length.gte', 2) + }) + }) + }) +}) diff --git a/cypress/e2e/insights-navigation.cy.ts b/cypress/e2e/insights-navigation.cy.ts index 00ec829321bed..57a1853841533 100644 --- a/cypress/e2e/insights-navigation.cy.ts +++ b/cypress/e2e/insights-navigation.cy.ts @@ -48,101 +48,6 @@ describe('Insights', () => { cy.get('.RetentionTable__Tab').should('have.length', 66) }) - describe('opening a new insight directly', () => { - it('can open a new trends insight', () => { - insight.newInsight('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') - cy.get('tr').should('have.length.gte', 2) - }) - - it('can open a new funnels insight', () => { - insight.newInsight('FUNNELS') - cy.get('.funnels-empty-state__title').should('exist') - }) - - it.skip('can open a new retention insight', () => { - insight.newInsight('RETENTION') - cy.get('.RetentionContainer canvas').should('exist') - cy.get('.RetentionTable__Tab').should('have.length', 66) - }) - - it('can open a new paths insight', () => { - insight.newInsight('PATHS') - cy.get('.Paths g').should('have.length.gte', 5) // not a fixed value unfortunately - }) - - it('can open a new stickiness insight', () => { - insight.newInsight('STICKINESS') - cy.get('.trends-insights-container canvas').should('exist') - }) - - it('can open a new lifecycle insight', () => { - insight.newInsight('LIFECYCLE') - cy.get('.trends-insights-container canvas').should('exist') - }) - - it('can open a new SQL insight', () => { - insight.newInsight('SQL') - insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') - cy.get('[data-attr="hogql-query-editor"]').should('exist') - cy.get('tr.DataTable__row').should('have.length.gte', 2) - }) - }) - - describe('opening a new insight after opening a new SQL insight', () => { - // TRICKY: these tests have identical assertions to the ones above, but we need to open a SQL insight first - // and then click a different tab to switch to that insight. - // this is because we had a bug where doing that would mean after starting to load the new insight, - // the SQL insight would be unexpectedly re-selected and the page would switch back to it - - beforeEach(() => { - insight.newInsight('SQL') - insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') - cy.get('[data-attr="hogql-query-editor"]').should('exist') - cy.get('tr.DataTable__row').should('have.length.gte', 2) - }) - - it('can open a new trends insight', () => { - insight.clickTab('TRENDS') - cy.get('.trends-insights-container canvas').should('exist') - cy.get('tr').should('have.length.gte', 2) - cy.contains('tr', 'No insight results').should('not.exist') - }) - - it('can open a new funnels insight', () => { - insight.clickTab('FUNNELS') - cy.get('.funnels-empty-state__title').should('exist') - }) - - it('can open a new retention insight', () => { - insight.clickTab('RETENTION') - cy.get('.RetentionContainer canvas').should('exist') - cy.get('.RetentionTable__Tab').should('have.length', 66) - }) - - it('can open a new paths insight', () => { - insight.clickTab('PATH') - cy.get('.Paths g').should('have.length.gte', 5) // not a fixed value unfortunately - }) - - it('can open a new stickiness insight', () => { - insight.clickTab('STICKINESS') - cy.get('.trends-insights-container canvas').should('exist') - }) - - it('can open a new lifecycle insight', () => { - insight.clickTab('LIFECYCLE') - cy.get('.trends-insights-container canvas').should('exist') - }) - - it('can open a new SQL insight', () => { - insight.clickTab('SQL') - insight.updateQueryEditorText(hogQLQuery, 'hogql-query-editor') - cy.get('[data-attr="hogql-query-editor"]').should('exist') - cy.get('tr.DataTable__row').should('have.length.gte', 2) - }) - }) - it('can open a new SQL insight and navigate to a different one, then back to SQL, and back again', () => { /** * This is here as a regression test. We had a bug where navigating to a new query based insight, diff --git a/cypress/e2e/insights.cy.ts b/cypress/e2e/insights.cy.ts index 5e4fc0063855b..bbffc9558e1e9 100644 --- a/cypress/e2e/insights.cy.ts +++ b/cypress/e2e/insights.cy.ts @@ -131,25 +131,6 @@ describe('Insights', () => { cy.get('[data-attr=insight-tags]').should('not.exist') }) - describe('insights date picker', () => { - it('Can set the date filter and show the right grouping interval', () => { - cy.get('[data-attr=date-filter]').click() - cy.get('div').contains('Yesterday').should('exist').click() - cy.get('[data-attr=interval-filter]').should('contain', 'Hour') - }) - - it('Can set a custom rolling date range', () => { - cy.get('[data-attr=date-filter]').click() - cy.get('[data-attr=rolling-date-range-input]').type('{selectall}5{enter}') - cy.get('[data-attr=rolling-date-range-date-options-selector]').click() - cy.get('.RollingDateRangeFilter__popover > div').contains('days').should('exist').click() - cy.get('.RollingDateRangeFilter__label').should('contain', 'In the last').click() - - // Test that the button shows the correct formatted range - cy.get('[data-attr=date-filter]').get('span').contains('Last 5 days').should('exist') - }) - }) - describe('view source', () => { it('can open the query editor', () => { insight.newInsight('TRENDS') diff --git a/cypress/e2e/notebooks-creation-and-deletion.cy.ts b/cypress/e2e/notebooks-creation-and-deletion.cy.ts new file mode 100644 index 0000000000000..6206880118a81 --- /dev/null +++ b/cypress/e2e/notebooks-creation-and-deletion.cy.ts @@ -0,0 +1,43 @@ +import { randomString } from '../support/random' + +function visitNotebooksList(): void { + cy.clickNavMenu('dashboards') + cy.location('pathname').should('include', '/dashboard') + cy.get('h1').should('contain', 'Dashboards & Notebooks') + cy.get('li').contains('Notebooks').should('exist').click() +} + +function createNotebookAndFindInList(notebookTitle: string): void { + cy.get('[data-attr="new-notebook"]').click() + cy.get('.NotebookEditor').type(notebookTitle) + + visitNotebooksList() + cy.get('[data-attr="notebooks-search"]').type(notebookTitle) +} + +describe('Notebooks', () => { + beforeEach(() => { + visitNotebooksList() + }) + + it('can create and name a notebook', () => { + const notebookTitle = randomString('My new notebook') + + createNotebookAndFindInList(notebookTitle) + cy.get('[data-attr="notebooks-table"] tbody tr').should('have.length', 1) + }) + + it('can delete a notebook', () => { + const notebookTitle = randomString('My notebook to delete') + + createNotebookAndFindInList(notebookTitle) + + cy.contains('[data-attr="notebooks-table"] tr', notebookTitle).within(() => { + cy.get('[aria-label="more"]').click() + }) + cy.contains('.LemonButton', 'Delete').click() + + // and the table updates + cy.contains('[data-attr="notebooks-table"] tr', notebookTitle).should('not.exist') + }) +}) diff --git a/cypress/e2e/notebooks.cy.ts b/cypress/e2e/notebooks.cy.ts index d44555d42294e..7aba143661d54 100644 --- a/cypress/e2e/notebooks.cy.ts +++ b/cypress/e2e/notebooks.cy.ts @@ -7,16 +7,23 @@ describe('Notebooks', () => { 'loadSessionRecordingsList' ) }) + cy.fixture('api/session-recordings/recording.json').then((recording) => { cy.intercept('GET', /api\/projects\/\d+\/session_recordings\/.*\?.*/, { body: recording }).as( 'loadSessionRecording' ) }) + cy.fixture('api/notebooks/notebooks.json').then((notebook) => { cy.intercept('GET', /api\/projects\/\d+\/notebooks\//, { body: notebook }).as('loadNotebooksList') }) + cy.fixture('api/notebooks/notebook.json').then((notebook) => { cy.intercept('GET', /api\/projects\/\d+\/notebooks\/.*\//, { body: notebook }).as('loadNotebook') + // this means saving doesn't work but so what? + cy.intercept('PATCH', /api\/projects\/\d+\/notebooks\/.*\//, (req, res) => { + res.reply(req.body) + }).as('patchNotebook') }) cy.clickNavMenu('dashboards') @@ -53,4 +60,39 @@ describe('Notebooks', () => { cy.get('.ph-recording.NotebookNode').should('be.visible') cy.get('.NotebookRecordingTimestamp').should('contain.text', '0:00') }) + + describe('text types', () => { + beforeEach(() => { + cy.get('li').contains('Notebooks').should('exist').click() + cy.get('[data-attr="new-notebook"]').click() + // we don't actually get a new notebook because the API is mocked + // so, "exit" the timestamp block we start in + cy.get('.NotebookEditor').type('{esc}{enter}{enter}') + }) + + it('Can add a number list', () => { + cy.get('.NotebookEditor').type('1. the first') + cy.get('.NotebookEditor').type('{enter}') + // no need to type the number now. it should be inserted automatically + cy.get('.NotebookEditor').type('the second') + cy.get('.NotebookEditor').type('{enter}') + cy.get('ol').should('contain.text', 'the first') + cy.get('ol').should('contain.text', 'the second') + // the numbered list auto inserts the next list item + cy.get('.NotebookEditor ol li').should('have.length', 3) + }) + + it('Can add bold', () => { + cy.get('.NotebookEditor').type('**bold**') + cy.get('.NotebookEditor p').last().should('contain.html', 'bold') + }) + + it('Can add bullet list', () => { + cy.get('.NotebookEditor').type('* the first{enter}the second{enter}') + cy.get('ul').should('contain.text', 'the first') + cy.get('ul').should('contain.text', 'the second') + // the list auto inserts the next list item + cy.get('.NotebookEditor ul li').should('have.length', 3) + }) + }) }) diff --git a/docker-compose.base.yml b/docker-compose.base.yml index a5e3e44396f84..dba92a0046034 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -35,7 +35,7 @@ services: restart: on-failure kafka: - image: bitnami/kafka:2.8.1-debian-10-r99 + image: ghcr.io/posthog/kafka-container:v2.8.2 restart: on-failure depends_on: - zookeeper diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 84f0f1c827174..2045dee0804c5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -46,6 +46,8 @@ services: extends: file: docker-compose.base.yml service: zookeeper + ports: + - '2181:2181' kafka: extends: diff --git a/docker-compose.hobby.yml b/docker-compose.hobby.yml index cc61b627e0a0c..bf63efa21e0b2 100644 --- a/docker-compose.hobby.yml +++ b/docker-compose.hobby.yml @@ -13,8 +13,11 @@ services: extends: file: docker-compose.base.yml service: db + # Pin to postgres 12 until we have a process for pg_upgrade to postgres 15 for exsisting installations + image: ${DOCKER_REGISTRY_PREFIX:-}postgres:12-alpine volumes: - postgres-data:/var/lib/postgresql/data + redis: extends: file: docker-compose.base.yml diff --git a/ee/api/test/test_instance_settings.py b/ee/api/test/test_instance_settings.py index 33cf5b450d6f8..de391bfc923a7 100644 --- a/ee/api/test/test_instance_settings.py +++ b/ee/api/test/test_instance_settings.py @@ -5,7 +5,7 @@ from posthog.client import sync_execute from posthog.models.instance_setting import get_instance_setting from posthog.models.performance.sql import PERFORMANCE_EVENT_DATA_TABLE -from posthog.models.session_recording_event.sql import SESSION_RECORDING_EVENTS_DATA_TABLE +from posthog.session_recordings.sql.session_recording_event_sql import SESSION_RECORDING_EVENTS_DATA_TABLE from posthog.settings.data_stores import CLICKHOUSE_DATABASE from posthog.test.base import ClickhouseTestMixin, snapshot_clickhouse_alter_queries 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 76b856caa0287..9f9e01f13028a 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr @@ -1,6 +1,6 @@ # name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results ' - /* user_id:51 celery:posthog.celery.sync_insight_caching_state */ + /* user_id:128 celery:posthog.celery.sync_insight_caching_state */ SELECT team_id, date_diff('second', max(timestamp), now()) AS age FROM events @@ -12,50 +12,78 @@ --- # name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results.1 ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') AS value, + count(*) as count + FROM events e + WHERE team_id = 2 + AND event = '$pageview' + 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 25 + OFFSET 0) ' --- # name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results.2 ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(day_start) as date, + groupArray(count) AS total, + breakdown_value + FROM + (SELECT SUM(total) as count, + day_start, + breakdown_value + FROM + (SELECT * + FROM + (SELECT toUInt16(0) AS total, + ticks.day_start as day_start, + breakdown_value + FROM + (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start + FROM numbers(6) + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks + CROSS JOIN + (SELECT breakdown_value + FROM + (SELECT ['control', 'test', 'ablahebf', ''] as breakdown_value) ARRAY + JOIN breakdown_value) as sec + ORDER BY breakdown_value, + day_start + UNION ALL SELECT count(*) as total, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, + replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') as breakdown_value + FROM events e + WHERE e.team_id = 2 + AND event = '$pageview' + 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['control', 'test', 'ablahebf', '']) + GROUP BY day_start, + breakdown_value)) + GROUP BY day_start, + breakdown_value + ORDER BY breakdown_value, + day_start) + GROUP BY breakdown_value + ORDER BY breakdown_value ' --- # name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results.3 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results.4 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) FROM - (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') AS value, + (SELECT array(replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '')) AS value, count(*) as count FROM events e WHERE team_id = 2 - AND event = '$pageview' + 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 @@ -64,6 +92,78 @@ OFFSET 0) ' --- +# name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results.4 + ' + /* 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 + 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.5 ' /* user_id:0 request:_snapshot_ */ diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr index 15bbb8312a341..d185a7a063790 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr @@ -1,25 +1,91 @@ # name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results ' - /* user_id:58 celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (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', '$pageview'] + 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 25 + OFFSET 0) ' --- # name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results.1 ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + /* 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', 1, 0) as step_0, + if(step_0 = 1, timestamp, null) as latest_0, + if(event = '$pageleave', 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 + 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', '$pageview'] + 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: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results.2 @@ -137,54 +203,6 @@ ' --- # name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones - ' - /* user_id:59 celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.1 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.2 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.3 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.4 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) @@ -202,7 +220,7 @@ OFFSET 0) ' --- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.5 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.1 ' /* user_id:0 request:_snapshot_ */ SELECT countIf(steps = 1) step_1, @@ -274,31 +292,7 @@ GROUP BY prop ' --- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants - ' - /* user_id:61 celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.1 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.2 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.2 ' /* celery:posthog.celery.sync_insight_caching_state */ SELECT team_id, @@ -310,7 +304,7 @@ ORDER BY age; ' --- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.3 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.3 ' /* celery:posthog.celery.sync_insight_caching_state */ SELECT team_id, @@ -322,7 +316,7 @@ ORDER BY age; ' --- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.4 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.4 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) @@ -332,15 +326,15 @@ FROM events e WHERE team_id = 2 AND event IN ['$pageleave', '$pageview'] - 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 toTimeZone(timestamp, 'Europe/Amsterdam') >= toDateTime('2020-01-01 14:20:21', 'Europe/Amsterdam') + AND toTimeZone(timestamp, 'Europe/Amsterdam') <= toDateTime('2020-01-06 10:00:00', 'Europe/Amsterdam') GROUP BY value ORDER BY count DESC, value DESC LIMIT 25 OFFSET 0) ' --- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.5 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones.5 ' /* user_id:0 request:_snapshot_ */ SELECT countIf(steps = 1) step_1, @@ -375,7 +369,7 @@ 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_1'], ['test'], ['control'], ['unknown_3'], ['unknown_2'], ['unknown_1'], ['test_2']], prop), prop, ['Other']) as prop + if(has([['test'], ['control']], prop), prop, ['Other']) as prop FROM (SELECT *, if(notEmpty(arrayFilter(x -> notEmpty(x), prop_vals)), prop_vals, ['']) as prop @@ -400,8 +394,8 @@ HAVING argMax(is_deleted, version) = 0) AS pdi ON e.distinct_id = pdi.distinct_id WHERE team_id = 2 AND event IN ['$pageleave', '$pageview'] - 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 toTimeZone(timestamp, 'Europe/Amsterdam') >= toDateTime('2020-01-01 14:20:21', 'Europe/Amsterdam') + AND toTimeZone(timestamp, 'Europe/Amsterdam') <= toDateTime('2020-01-06 10:00:00', 'Europe/Amsterdam') AND (step_0 = 1 OR step_1 = 1) ))) WHERE step_0 = 1 )) @@ -412,55 +406,7 @@ GROUP BY prop ' --- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation - ' - /* user_id:62 celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.1 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.2 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.3 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.4 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) @@ -478,7 +424,7 @@ OFFSET 0) ' --- -# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.5 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.1 ' /* user_id:0 request:_snapshot_ */ SELECT countIf(steps = 1) step_1, @@ -513,13 +459,13 @@ 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 + if(has([[''], ['test_1'], ['test'], ['control'], ['unknown_3'], ['unknown_2'], ['unknown_1'], ['test_2']], 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, - replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$account_id'), ''), 'null'), '^"|"$', '') as aggregation_target, + pdi.person_id as aggregation_target, pdi.person_id as person_id , if(event = '$pageview', 1, 0) as step_0, if(step_0 = 1, timestamp, null) as latest_0, @@ -550,9 +496,9 @@ GROUP BY prop ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.2 ' - /* user_id:65 celery:posthog.celery.sync_insight_caching_state */ + /* celery:posthog.celery.sync_insight_caching_state */ SELECT team_id, date_diff('second', max(timestamp), now()) AS age FROM events @@ -562,7 +508,7 @@ ORDER BY age; ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.1 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.3 ' /* celery:posthog.celery.sync_insight_caching_state */ SELECT team_id, @@ -574,31 +520,694 @@ ORDER BY age; ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.2 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.4 ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (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', '$pageview'] + 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 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.5 + ' + /* 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_1'], ['test'], ['control'], ['unknown_3'], ['unknown_2'], ['unknown_1'], ['test_2']], 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', 1, 0) as step_0, + if(step_0 = 1, timestamp, null) as latest_0, + if(event = '$pageleave', 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 + 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', '$pageview'] + 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: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (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', '$pageview'] + 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 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.1 + ' + /* 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, + replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$account_id'), ''), 'null'), '^"|"$', '') as aggregation_target, + pdi.person_id as person_id , + if(event = '$pageview', 1, 0) as step_0, + if(step_0 = 1, timestamp, null) as latest_0, + if(event = '$pageleave', 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 + 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', '$pageview'] + 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: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.2 + ' + /* celery:posthog.celery.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id + ORDER BY age; + ' +--- +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.3 + ' + /* celery:posthog.celery.sync_insight_caching_state */ + SELECT team_id, + date_diff('second', max(timestamp), now()) AS age + FROM events + WHERE timestamp > date_sub(DAY, 3, now()) + AND timestamp < now() + GROUP BY team_id ORDER BY age; ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.3 +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.4 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (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', '$pageview'] + 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 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_with_hogql_aggregation.5 + ' + /* 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, + replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$account_id'), ''), 'null'), '^"|"$', '') as aggregation_target, + pdi.person_id as person_id , + if(event = '$pageview', 1, 0) as step_0, + if(step_0 = 1, timestamp, null) as latest_0, + if(event = '$pageleave', 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 + 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', '$pageview'] + 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: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') AS value, + count(*) as count + FROM events e + WHERE team_id = 2 + AND event = '$pageview' + 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 (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + GROUP BY value + ORDER BY count DESC, value DESC + LIMIT 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.1 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(day_start) as date, + groupArray(count) AS total, + breakdown_value + FROM + (SELECT SUM(total) as count, + day_start, + breakdown_value + FROM + (SELECT * + FROM + (SELECT toUInt16(0) AS total, + ticks.day_start as day_start, + breakdown_value + FROM + (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start + FROM numbers(6) + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks + CROSS JOIN + (SELECT breakdown_value + FROM + (SELECT ['test', 'control'] as breakdown_value) ARRAY + JOIN breakdown_value) as sec + ORDER BY breakdown_value, + day_start + UNION ALL SELECT count(*) as total, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, + replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') as breakdown_value + FROM events e + WHERE e.team_id = 2 + AND event = '$pageview' + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['test', 'control']) + GROUP BY day_start, + breakdown_value)) + GROUP BY day_start, + breakdown_value + ORDER BY breakdown_value, + day_start) + GROUP BY breakdown_value + ORDER BY breakdown_value + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.2 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') AS value, + count(*) as count + FROM events e + WHERE team_id = 2 + AND event = '$feature_flag_called' + 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 (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) + AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) + GROUP BY value + ORDER BY count DESC, value DESC + LIMIT 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.3 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(day_start) as date, + groupArray(count) AS total, + breakdown_value + FROM + (SELECT SUM(total) as count, + day_start, + breakdown_value + FROM + (SELECT * + FROM + (SELECT toUInt16(0) AS total, + ticks.day_start as day_start, + breakdown_value + FROM + (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start + FROM numbers(6) + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks + CROSS JOIN + (SELECT breakdown_value + FROM + (SELECT ['control', 'test'] as breakdown_value) ARRAY + JOIN breakdown_value) as sec + ORDER BY breakdown_value, + day_start + UNION ALL SELECT count(DISTINCT person_id) as total, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, + breakdown_value + FROM + (SELECT person_id, + min(timestamp) as timestamp, + breakdown_value + FROM + (SELECT pdi.person_id as person_id, timestamp, replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') as breakdown_value + FROM events e + INNER JOIN + (SELECT distinct_id, + argMax(person_id, version) as person_id + FROM person_distinct_id2 + WHERE team_id = 2 + GROUP BY distinct_id + HAVING argMax(is_deleted, version) = 0) as pdi ON events.distinct_id = pdi.distinct_id + WHERE e.team_id = 2 + AND event = '$feature_flag_called' + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) + AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) + 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') in (['control', 'test']) ) + GROUP BY person_id, + breakdown_value) AS pdi + GROUP BY day_start, + breakdown_value)) + GROUP BY day_start, + breakdown_value + ORDER BY breakdown_value, + day_start) + GROUP BY breakdown_value + ORDER BY breakdown_value + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.4 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') AS value, + count(*) as count + FROM events e + WHERE team_id = 2 + AND event = '$pageview' + 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 (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + GROUP BY value + ORDER BY count DESC, value DESC + LIMIT 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.5 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(day_start) as date, + groupArray(count) AS total, + breakdown_value + FROM + (SELECT SUM(total) as count, + day_start, + breakdown_value + FROM + (SELECT * + FROM + (SELECT toUInt16(0) AS total, + ticks.day_start as day_start, + breakdown_value + FROM + (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start + FROM numbers(6) + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks + CROSS JOIN + (SELECT breakdown_value + FROM + (SELECT ['test', 'control'] as breakdown_value) ARRAY + JOIN breakdown_value) as sec + ORDER BY breakdown_value, + day_start + UNION ALL SELECT count(*) as total, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, + replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') as breakdown_value + FROM events e + WHERE e.team_id = 2 + AND event = '$pageview' + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['test', 'control']) + GROUP BY day_start, + breakdown_value)) + GROUP BY day_start, + breakdown_value + ORDER BY breakdown_value, + day_start) + GROUP BY breakdown_value + ORDER BY breakdown_value + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.6 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') AS value, + count(*) as count + FROM events e + WHERE team_id = 2 + AND event = '$feature_flag_called' + 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 (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) + AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) + GROUP BY value + ORDER BY count DESC, value DESC + LIMIT 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.7 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(day_start) as date, + groupArray(count) AS total, + breakdown_value + FROM + (SELECT SUM(total) as count, + day_start, + breakdown_value + FROM + (SELECT * + FROM + (SELECT toUInt16(0) AS total, + ticks.day_start as day_start, + breakdown_value + FROM + (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start + FROM numbers(6) + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks + CROSS JOIN + (SELECT breakdown_value + FROM + (SELECT ['control', 'test'] as breakdown_value) ARRAY + JOIN breakdown_value) as sec + ORDER BY breakdown_value, + day_start + UNION ALL SELECT count(DISTINCT person_id) as total, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, + breakdown_value + FROM + (SELECT person_id, + min(timestamp) as timestamp, + breakdown_value + FROM + (SELECT pdi.person_id as person_id, timestamp, replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') as breakdown_value + FROM events e + INNER JOIN + (SELECT distinct_id, + argMax(person_id, version) as person_id + FROM person_distinct_id2 + WHERE team_id = 2 + GROUP BY distinct_id + HAVING argMax(is_deleted, version) = 0) as pdi ON events.distinct_id = pdi.distinct_id + WHERE e.team_id = 2 + AND event = '$feature_flag_called' + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) + AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) + 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') in (['control', 'test']) ) + GROUP BY person_id, + breakdown_value) AS pdi + GROUP BY day_start, + breakdown_value)) + GROUP BY day_start, + breakdown_value + ORDER BY breakdown_value, + day_start) + GROUP BY breakdown_value + ORDER BY breakdown_value + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') AS value, + count(*) as count + FROM events e + WHERE team_id = 2 + AND event = '$pageview1' + 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 (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + GROUP BY value + ORDER BY count DESC, value DESC + LIMIT 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.1 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(day_start) as date, + groupArray(count) AS total, + breakdown_value + FROM + (SELECT SUM(total) as count, + day_start, + breakdown_value + FROM + (SELECT * + FROM + (SELECT toUInt16(0) AS total, + ticks.day_start as day_start, + breakdown_value + FROM + (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start + FROM numbers(6) + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks + CROSS JOIN + (SELECT breakdown_value + FROM + (SELECT ['control', 'test_1', 'test_2'] as breakdown_value) ARRAY + JOIN breakdown_value) as sec + ORDER BY breakdown_value, + day_start + UNION ALL SELECT count(*) as total, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, + replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') as breakdown_value + FROM events e + WHERE e.team_id = 2 + AND event = '$pageview1' + AND (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['control', 'test_1', 'test_2']) + GROUP BY day_start, + breakdown_value)) + GROUP BY day_start, + breakdown_value + ORDER BY breakdown_value, + day_start) + GROUP BY breakdown_value + ORDER BY breakdown_value + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.2 ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') AS value, + count(*) as count + FROM events e + WHERE team_id = 2 + AND event = '$feature_flag_called' + 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 (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) + AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) + GROUP BY value + ORDER BY count DESC, value DESC + LIMIT 25 + OFFSET 0) ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.4 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.3 + ' + /* user_id:0 request:_snapshot_ */ + SELECT [now()] AS date, + [0] AS total, + '' AS breakdown_value + LIMIT 0 + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.4 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) @@ -607,17 +1216,17 @@ count(*) as count FROM events e WHERE team_id = 2 - AND event = '$pageview' + AND event = '$pageview1' 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 (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + AND (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC LIMIT 25 OFFSET 0) ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.5 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.5 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(day_start) as date, @@ -640,7 +1249,7 @@ CROSS JOIN (SELECT breakdown_value FROM - (SELECT ['test', 'control'] as breakdown_value) ARRAY + (SELECT ['control', 'test_1', 'test_2'] as breakdown_value) ARRAY JOIN breakdown_value) as sec ORDER BY breakdown_value, day_start @@ -649,11 +1258,11 @@ replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') as breakdown_value FROM events e WHERE e.team_id = 2 - AND event = '$pageview' - AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + AND event = '$pageview1' + AND (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['test', 'control']) + AND replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['control', 'test_1', 'test_2']) GROUP BY day_start, breakdown_value)) GROUP BY day_start, @@ -664,7 +1273,7 @@ ORDER BY breakdown_value ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.6 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.6 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) @@ -676,6 +1285,101 @@ AND event = '$feature_flag_called' 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 (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) + AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) + GROUP BY value + ORDER BY count DESC, value DESC + LIMIT 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.7 + ' + /* user_id:0 request:_snapshot_ */ + SELECT [now()] AS date, + [0] AS total, + '' AS breakdown_value + LIMIT 0 + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') AS value, + count(*) as count + FROM events e + WHERE team_id = 2 + AND event = '$pageview' + AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') + AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + GROUP BY value + ORDER BY count DESC, value DESC + LIMIT 25 + OFFSET 0) + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.1 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(day_start) as date, + groupArray(count) AS total, + breakdown_value + FROM + (SELECT SUM(total) as count, + day_start, + breakdown_value + FROM + (SELECT * + FROM + (SELECT toUInt16(0) AS total, + ticks.day_start as day_start, + breakdown_value + FROM + (SELECT toStartOfDay(toDateTime('2020-01-06 07:00:00', 'US/Pacific')) - toIntervalDay(number) as day_start + FROM numbers(6) + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 02:10:00', 'US/Pacific')) as day_start) as ticks + CROSS JOIN + (SELECT breakdown_value + FROM + (SELECT ['test', 'control'] as breakdown_value) ARRAY + JOIN breakdown_value) as sec + ORDER BY breakdown_value, + day_start + UNION ALL SELECT count(*) as total, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'US/Pacific')) as day_start, + replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') as breakdown_value + FROM events e + WHERE e.team_id = 2 + AND event = '$pageview' + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') + AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + AND replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['test', 'control']) + GROUP BY day_start, + breakdown_value)) + GROUP BY day_start, + breakdown_value + ORDER BY breakdown_value, + day_start) + GROUP BY breakdown_value + ORDER BY breakdown_value + ' +--- +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.2 + ' + /* user_id:0 request:_snapshot_ */ + SELECT groupArray(value) + FROM + (SELECT replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') AS value, + count(*) as count + FROM events e + WHERE team_id = 2 + AND event = '$feature_flag_called' + AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') + AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) GROUP BY value @@ -684,7 +1388,7 @@ OFFSET 0) ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results.7 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.3 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(day_start) as date, @@ -701,9 +1405,9 @@ ticks.day_start as day_start, breakdown_value FROM - (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start + (SELECT toStartOfDay(toDateTime('2020-01-06 07:00:00', 'US/Pacific')) - toIntervalDay(number) as day_start FROM numbers(6) - UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 02:10:00', 'US/Pacific')) as day_start) as ticks CROSS JOIN (SELECT breakdown_value FROM @@ -712,7 +1416,7 @@ ORDER BY breakdown_value, day_start UNION ALL SELECT count(DISTINCT person_id) as total, - toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'US/Pacific')) as day_start, breakdown_value FROM (SELECT person_id, @@ -729,73 +1433,25 @@ GROUP BY distinct_id HAVING argMax(is_deleted, version) = 0) as pdi ON events.distinct_id = pdi.distinct_id WHERE e.team_id = 2 - AND event = '$feature_flag_called' - AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) - AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) - 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') in (['control', 'test']) ) - GROUP BY person_id, - breakdown_value) AS pdi - GROUP BY day_start, - breakdown_value)) - GROUP BY day_start, - breakdown_value - ORDER BY breakdown_value, - day_start) - GROUP BY breakdown_value - ORDER BY breakdown_value - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants - ' - /* user_id:66 celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.1 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.2 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.3 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + AND event = '$feature_flag_called' + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) + AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) + AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') + AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + AND replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') in (['control', 'test']) ) + GROUP BY person_id, + breakdown_value) AS pdi + GROUP BY day_start, + breakdown_value)) + GROUP BY day_start, + breakdown_value + ORDER BY breakdown_value, + day_start) + GROUP BY breakdown_value + ORDER BY breakdown_value ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.4 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.4 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) @@ -804,17 +1460,17 @@ count(*) as count FROM events e WHERE team_id = 2 - AND event = '$pageview1' - 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 (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + AND event = '$pageview' + AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') + AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC LIMIT 25 OFFSET 0) ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.5 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.5 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(day_start) as date, @@ -831,26 +1487,26 @@ ticks.day_start as day_start, breakdown_value FROM - (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start + (SELECT toStartOfDay(toDateTime('2020-01-06 07:00:00', 'US/Pacific')) - toIntervalDay(number) as day_start FROM numbers(6) - UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 02:10:00', 'US/Pacific')) as day_start) as ticks CROSS JOIN (SELECT breakdown_value FROM - (SELECT ['control', 'test_1', 'test_2'] as breakdown_value) ARRAY + (SELECT ['test', 'control'] as breakdown_value) ARRAY JOIN breakdown_value) as sec ORDER BY breakdown_value, day_start UNION ALL SELECT count(*) as total, - toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'US/Pacific')) as day_start, replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') as breakdown_value FROM events e WHERE e.team_id = 2 - AND event = '$pageview1' - AND (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) - 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['control', 'test_1', 'test_2']) + AND event = '$pageview' + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') + AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + AND replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['test', 'control']) GROUP BY day_start, breakdown_value)) GROUP BY day_start, @@ -861,7 +1517,7 @@ ORDER BY breakdown_value ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.6 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.6 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) @@ -871,9 +1527,9 @@ FROM events e WHERE team_id = 2 AND event = '$feature_flag_called' - 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 (has(['control', 'test_1', 'test_2', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) + AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') + AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) GROUP BY value ORDER BY count DESC, value DESC @@ -881,64 +1537,70 @@ OFFSET 0) ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants.7 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.7 ' /* user_id:0 request:_snapshot_ */ - SELECT [now()] AS date, - [0] AS total, - '' AS breakdown_value - LIMIT 0 - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone - ' - /* user_id:68 celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.1 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.2 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.3 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; + SELECT groupArray(day_start) as date, + groupArray(count) AS total, + breakdown_value + FROM + (SELECT SUM(total) as count, + day_start, + breakdown_value + FROM + (SELECT * + FROM + (SELECT toUInt16(0) AS total, + ticks.day_start as day_start, + breakdown_value + FROM + (SELECT toStartOfDay(toDateTime('2020-01-06 07:00:00', 'US/Pacific')) - toIntervalDay(number) as day_start + FROM numbers(6) + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 02:10:00', 'US/Pacific')) as day_start) as ticks + CROSS JOIN + (SELECT breakdown_value + FROM + (SELECT ['control', 'test'] as breakdown_value) ARRAY + JOIN breakdown_value) as sec + ORDER BY breakdown_value, + day_start + UNION ALL SELECT count(DISTINCT person_id) as total, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'US/Pacific')) as day_start, + breakdown_value + FROM + (SELECT person_id, + min(timestamp) as timestamp, + breakdown_value + FROM + (SELECT pdi.person_id as person_id, timestamp, replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') as breakdown_value + FROM events e + INNER JOIN + (SELECT distinct_id, + argMax(person_id, version) as person_id + FROM person_distinct_id2 + WHERE team_id = 2 + GROUP BY distinct_id + HAVING argMax(is_deleted, version) = 0) as pdi ON events.distinct_id = pdi.distinct_id + WHERE e.team_id = 2 + AND event = '$feature_flag_called' + AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) + AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) + AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') + AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + AND replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') in (['control', 'test']) ) + GROUP BY person_id, + breakdown_value) AS pdi + GROUP BY day_start, + breakdown_value)) + GROUP BY day_start, + breakdown_value + ORDER BY breakdown_value, + day_start) + GROUP BY breakdown_value + ORDER BY breakdown_value ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.4 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_with_hogql_filter ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) @@ -948,16 +1610,18 @@ FROM events e WHERE team_id = 2 AND event = '$pageview' - AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') - AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') - AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + 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 ((has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', '')) + and isNull('true')))) GROUP BY value ORDER BY count DESC, value DESC LIMIT 25 OFFSET 0) ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.5 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_with_hogql_filter.1 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(day_start) as date, @@ -974,9 +1638,9 @@ ticks.day_start as day_start, breakdown_value FROM - (SELECT toStartOfDay(toDateTime('2020-01-06 07:00:00', 'US/Pacific')) - toIntervalDay(number) as day_start + (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start FROM numbers(6) - UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 02:10:00', 'US/Pacific')) as day_start) as ticks + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks CROSS JOIN (SELECT breakdown_value FROM @@ -985,14 +1649,16 @@ ORDER BY breakdown_value, day_start UNION ALL SELECT count(*) as total, - toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'US/Pacific')) as day_start, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') as breakdown_value FROM events e WHERE e.team_id = 2 AND event = '$pageview' - AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) - AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') - AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + AND ((has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) + AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', '')) + and isNull('true')))) + 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['test', 'control']) GROUP BY day_start, breakdown_value)) @@ -1004,7 +1670,7 @@ ORDER BY breakdown_value ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.6 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_with_hogql_filter.2 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) @@ -1014,8 +1680,8 @@ FROM events e WHERE team_id = 2 AND event = '$feature_flag_called' - AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') - AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + 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 (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) GROUP BY value @@ -1024,7 +1690,7 @@ OFFSET 0) ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone.7 +# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_with_hogql_filter.3 ' /* user_id:0 request:_snapshot_ */ SELECT groupArray(day_start) as date, @@ -1041,9 +1707,9 @@ ticks.day_start as day_start, breakdown_value FROM - (SELECT toStartOfDay(toDateTime('2020-01-06 07:00:00', 'US/Pacific')) - toIntervalDay(number) as day_start + (SELECT toStartOfDay(toDateTime('2020-01-06 00:00:00', 'UTC')) - toIntervalDay(number) as day_start FROM numbers(6) - UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 02:10:00', 'US/Pacific')) as day_start) as ticks + UNION ALL SELECT toStartOfDay(toDateTime('2020-01-01 00:00:00', 'UTC')) as day_start) as ticks CROSS JOIN (SELECT breakdown_value FROM @@ -1052,7 +1718,7 @@ ORDER BY breakdown_value, day_start UNION ALL SELECT count(DISTINCT person_id) as total, - toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'US/Pacific')) as day_start, + toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, breakdown_value FROM (SELECT person_id, @@ -1072,8 +1738,8 @@ AND event = '$feature_flag_called' AND (has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag_response'), '^"|"$', '')) AND has(['a-b-test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature_flag'), '^"|"$', ''))) - AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2020-01-01 02:10:00', 'US/Pacific') - AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2020-01-06 07:00:00', 'US/Pacific') + 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 replaceRegexpAll(JSONExtractRaw(properties, '$feature_flag_response'), '^"|"$', '') in (['control', 'test']) ) GROUP BY person_id, breakdown_value) AS pdi @@ -1087,54 +1753,6 @@ ORDER BY breakdown_value ' --- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_with_hogql_filter - ' - /* user_id:70 celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_with_hogql_filter.1 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_with_hogql_filter.2 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- -# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_with_hogql_filter.3 - ' - /* celery:posthog.celery.sync_insight_caching_state */ - SELECT team_id, - date_diff('second', max(timestamp), now()) AS age - FROM events - WHERE timestamp > date_sub(DAY, 3, now()) - AND timestamp < now() - GROUP BY team_id - ORDER BY age; - ' ---- # name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_with_hogql_filter.4 ' /* user_id:0 request:_snapshot_ */ diff --git a/ee/clickhouse/views/test/test_clickhouse_experiments.py b/ee/clickhouse/views/test/test_clickhouse_experiments.py index 9f86ee3fe1d51..07764b83845d8 100644 --- a/ee/clickhouse/views/test/test_clickhouse_experiments.py +++ b/ee/clickhouse/views/test/test_clickhouse_experiments.py @@ -777,7 +777,7 @@ def test_used_in_experiment_is_populated_correctly_for_feature_flag_list(self) - ).json() # TODO: Make sure permission bool doesn't cause n + 1 - with self.assertNumQueries(11): + with self.assertNumQueries(12): response = self.client.get(f"/api/projects/{self.team.id}/feature_flags") self.assertEqual(response.status_code, status.HTTP_200_OK) result = response.json() diff --git a/posthog/queries/session_recordings/__init__.py b/ee/session_recordings/__init__.py similarity index 100% rename from posthog/queries/session_recordings/__init__.py rename to ee/session_recordings/__init__.py diff --git a/ee/tasks/session_recording/persistence.py b/ee/session_recordings/persistence_tasks.py similarity index 81% rename from ee/tasks/session_recording/persistence.py rename to ee/session_recordings/persistence_tasks.py index 7ab15991d8cb6..e409d9d318df9 100644 --- a/ee/tasks/session_recording/persistence.py +++ b/ee/session_recordings/persistence_tasks.py @@ -3,9 +3,9 @@ import structlog from django.utils import timezone -from ee.models.session_recording_extensions import persist_recording +from ee.session_recordings.session_recording_extensions import persist_recording from posthog.celery import app -from posthog.models.session_recording.session_recording import SessionRecording +from posthog.session_recordings.models.session_recording import SessionRecording logger = structlog.get_logger(__name__) diff --git a/ee/models/session_recording_extensions.py b/ee/session_recordings/session_recording_extensions.py similarity index 95% rename from ee/models/session_recording_extensions.py rename to ee/session_recordings/session_recording_extensions.py index 5fc7750411a59..f5d48fb8b285f 100644 --- a/ee/models/session_recording_extensions.py +++ b/ee/session_recordings/session_recording_extensions.py @@ -11,8 +11,8 @@ from posthog import settings from posthog.event_usage import report_team_action -from posthog.models.session_recording.metadata import PersistedRecordingV1 -from posthog.models.session_recording.session_recording import SessionRecording +from posthog.session_recordings.models.metadata import PersistedRecordingV1 +from posthog.session_recordings.models.session_recording import SessionRecording from posthog.session_recordings.session_recording_helpers import compress_to_string, decompress from posthog.storage import object_storage @@ -137,8 +137,8 @@ def load_persisted_recording(recording: SessionRecording) -> Optional[PersistedR # and will not be loaded here if not recording.storage_version: try: - content = object_storage.read(recording.object_storage_path) - decompressed = json.loads(decompress(content)) + content = object_storage.read(str(recording.object_storage_path)) + decompressed = json.loads(decompress(content)) if content else None logger.info( "Persisting recording load: loaded!", recording_id=recording.session_id, diff --git a/ee/api/session_recording_playlist.py b/ee/session_recordings/session_recording_playlist.py similarity index 99% rename from ee/api/session_recording_playlist.py rename to ee/session_recordings/session_recording_playlist.py index 473576c053172..72ee7915cb111 100644 --- a/ee/api/session_recording_playlist.py +++ b/ee/session_recordings/session_recording_playlist.py @@ -12,7 +12,7 @@ from posthog.api.forbid_destroy_model import ForbidDestroyModel from posthog.api.routing import StructuredViewSetMixin -from posthog.api.session_recording import list_recordings +from posthog.session_recordings.session_recording_api import list_recordings from posthog.api.shared import UserBasicSerializer from posthog.constants import SESSION_RECORDINGS_FILTER_IDS, AvailableFeature from posthog.models import SessionRecording, SessionRecordingPlaylist, SessionRecordingPlaylistItem, Team, User diff --git a/posthog/queries/session_recordings/test/test_session_recording_list.py b/ee/session_recordings/test/__init__.py similarity index 100% rename from posthog/queries/session_recordings/test/test_session_recording_list.py rename to ee/session_recordings/test/__init__.py diff --git a/ee/models/test/test_session_recording_extensions.py b/ee/session_recordings/test/test_session_recording_extensions.py similarity index 94% rename from ee/models/test/test_session_recording_extensions.py rename to ee/session_recordings/test/test_session_recording_extensions.py index de4d83ecc004c..e201e71a02563 100644 --- a/ee/models/test/test_session_recording_extensions.py +++ b/ee/session_recordings/test/test_session_recording_extensions.py @@ -7,11 +7,11 @@ from botocore.config import Config from freezegun import freeze_time -from ee.models.session_recording_extensions import load_persisted_recording, persist_recording -from posthog.models.session_recording.session_recording import SessionRecording -from posthog.models.session_recording_playlist.session_recording_playlist import SessionRecordingPlaylist -from posthog.models.session_recording_playlist_item.session_recording_playlist_item import SessionRecordingPlaylistItem -from posthog.queries.session_recordings.test.session_replay_sql import produce_replay_summary +from ee.session_recordings.session_recording_extensions import load_persisted_recording, persist_recording +from posthog.session_recordings.models.session_recording import SessionRecording +from posthog.session_recordings.models.session_recording_playlist import SessionRecordingPlaylist +from posthog.session_recordings.models.session_recording_playlist_item import SessionRecordingPlaylistItem +from posthog.session_recordings.queries.test.session_replay_sql import produce_replay_summary from posthog.session_recordings.test.test_factory import create_session_recording_events from posthog.settings import ( OBJECT_STORAGE_ENDPOINT, @@ -200,7 +200,7 @@ def test_persists_recording_from_blob_ingested_storage(self): f"{recording.build_object_storage_path('2023-08-01')}/c", ] - @patch("ee.models.session_recording_extensions.report_team_action") + @patch("ee.session_recordings.session_recording_extensions.report_team_action") def test_persist_tracks_correct_to_posthog(self, mock_capture): two_minutes_ago = (datetime.now() - timedelta(minutes=2)).replace(tzinfo=timezone.utc) diff --git a/ee/api/test/test_session_recording_playlist.py b/ee/session_recordings/test/test_session_recording_playlist.py similarity index 98% rename from ee/api/test/test_session_recording_playlist.py rename to ee/session_recordings/test/test_session_recording_playlist.py index 677cc83edc595..ddbb4d1195bca 100644 --- a/ee/api/test/test_session_recording_playlist.py +++ b/ee/session_recordings/test/test_session_recording_playlist.py @@ -12,7 +12,7 @@ from ee.api.test.base import APILicensedTest from ee.api.test.fixtures.available_product_features import AVAILABLE_PRODUCT_FEATURES from posthog.models import SessionRecording, SessionRecordingPlaylistItem -from posthog.models.session_recording_playlist.session_recording_playlist import SessionRecordingPlaylist +from posthog.session_recordings.models.session_recording_playlist import SessionRecordingPlaylist from posthog.models.user import User from posthog.session_recordings.test.test_factory import create_session_recording_events from posthog.settings import ( @@ -209,8 +209,8 @@ def test_get_pinned_recordings_for_playlist(self): assert {x["id"] for x in result["results"]} == {session_one, session_two} assert {x["pinned_count"] for x in result["results"]} == {1, 1} - @patch("ee.models.session_recording_extensions.object_storage.list_objects") - @patch("ee.models.session_recording_extensions.object_storage.copy_objects") + @patch("ee.session_recordings.session_recording_extensions.object_storage.list_objects") + @patch("ee.session_recordings.session_recording_extensions.object_storage.copy_objects") def test_fetch_playlist_recordings(self, mock_copy_objects: MagicMock, mock_list_objects: MagicMock) -> None: # all sessions have been blob ingested and had data to copy into the LTS storage location mock_copy_objects.return_value = 1 diff --git a/ee/tasks/__init__.py b/ee/tasks/__init__.py index e57f3c74cb8f9..dd549cd0c2789 100644 --- a/ee/tasks/__init__.py +++ b/ee/tasks/__init__.py @@ -1,4 +1,4 @@ -from .session_recording.persistence import persist_finished_recordings, persist_single_recording +from ee.session_recordings.persistence_tasks import persist_finished_recordings, persist_single_recording from .subscriptions import deliver_subscription_report, handle_subscription_value_change, schedule_all_subscriptions # As our EE tasks are not included at startup for Celery, we need to ensure they are declared here so that they are imported by posthog/settings/celery.py diff --git a/ee/urls.py b/ee/urls.py index 8179398ec1066..02f6028a1adcd 100644 --- a/ee/urls.py +++ b/ee/urls.py @@ -21,9 +21,9 @@ organization_resource_access, role, sentry_stats, - session_recording_playlist, subscription, ) +from .session_recordings import session_recording_playlist def extend_api_router( diff --git a/frontend/__snapshots__/components-cards-text-card--template.png b/frontend/__snapshots__/components-cards-text-card--template.png index 3e37496f1f7d9..ef9d5e2dfdf91 100644 Binary files a/frontend/__snapshots__/components-cards-text-card--template.png and b/frontend/__snapshots__/components-cards-text-card--template.png differ diff --git a/frontend/__snapshots__/components-editable-field--default.png b/frontend/__snapshots__/components-editable-field--default.png new file mode 100644 index 0000000000000..2d16114431388 Binary files /dev/null and b/frontend/__snapshots__/components-editable-field--default.png differ diff --git a/frontend/__snapshots__/components-editable-field--multiline-with-markdown.png b/frontend/__snapshots__/components-editable-field--multiline-with-markdown.png new file mode 100644 index 0000000000000..540e64f10339e Binary files /dev/null and b/frontend/__snapshots__/components-editable-field--multiline-with-markdown.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace.png b/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace.png index 2b399cfb52884..dd5f974f0ff1c 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace.png and b/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--importing-module.png b/frontend/__snapshots__/components-errors-error-display--importing-module.png index e624c68948256..7c9dfe5d3523d 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--importing-module.png and b/frontend/__snapshots__/components-errors-error-display--importing-module.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded.png b/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded.png index 12b8c9628a105..a8b33b29f5111 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded.png and b/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--safari-script-error.png b/frontend/__snapshots__/components-errors-error-display--safari-script-error.png index 7625b1fe6efe7..aca86b037f7c8 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--safari-script-error.png and b/frontend/__snapshots__/components-errors-error-display--safari-script-error.png differ diff --git a/frontend/__snapshots__/exporter-exporter--dashboard.png b/frontend/__snapshots__/exporter-exporter--dashboard.png index 9701d6fb63866..d46039284491f 100644 Binary files a/frontend/__snapshots__/exporter-exporter--dashboard.png and b/frontend/__snapshots__/exporter-exporter--dashboard.png differ diff --git a/frontend/__snapshots__/filters-taxonomic-filter--actions.png b/frontend/__snapshots__/filters-taxonomic-filter--actions.png index 282e7fecdd257..fff1fa4ce40b4 100644 Binary files a/frontend/__snapshots__/filters-taxonomic-filter--actions.png and b/frontend/__snapshots__/filters-taxonomic-filter--actions.png differ diff --git a/frontend/__snapshots__/filters-taxonomic-filter--events-free.png b/frontend/__snapshots__/filters-taxonomic-filter--events-free.png index 67ce48f256785..c1dd6086c9749 100644 Binary files a/frontend/__snapshots__/filters-taxonomic-filter--events-free.png and b/frontend/__snapshots__/filters-taxonomic-filter--events-free.png differ diff --git a/frontend/__snapshots__/filters-taxonomic-filter--events-premium.png b/frontend/__snapshots__/filters-taxonomic-filter--events-premium.png index d167e296b1cb8..417734adb3807 100644 Binary files a/frontend/__snapshots__/filters-taxonomic-filter--events-premium.png and b/frontend/__snapshots__/filters-taxonomic-filter--events-premium.png differ diff --git a/frontend/__snapshots__/filters-taxonomic-filter--properties.png b/frontend/__snapshots__/filters-taxonomic-filter--properties.png index 684cec6d7bca8..6166356899a25 100644 Binary files a/frontend/__snapshots__/filters-taxonomic-filter--properties.png and b/frontend/__snapshots__/filters-taxonomic-filter--properties.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png b/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png index 8f354b12f2b73..e81dc1cf31bbc 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png and b/frontend/__snapshots__/lemon-ui-lemon-file-input--default.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-markdown--default.png b/frontend/__snapshots__/lemon-ui-lemon-markdown--default.png new file mode 100644 index 0000000000000..b33258fa4950c Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-markdown--default.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-markdown--low-key-headings.png b/frontend/__snapshots__/lemon-ui-lemon-markdown--low-key-headings.png new file mode 100644 index 0000000000000..b870d48717d93 Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-lemon-markdown--low-key-headings.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-text-area--lemon-text-markdown.png b/frontend/__snapshots__/lemon-ui-lemon-text-area--lemon-text-markdown.png index de65bbdb2cfd7..8d0ee116d05ee 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-text-area--lemon-text-markdown.png and b/frontend/__snapshots__/lemon-ui-lemon-text-area--lemon-text-markdown.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--edit.png b/frontend/__snapshots__/scenes-app-dashboards--edit.png index d8df084f0b727..6389702cb99c5 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--edit.png and b/frontend/__snapshots__/scenes-app-dashboards--edit.png differ diff --git a/frontend/__snapshots__/scenes-app-dashboards--show.png b/frontend/__snapshots__/scenes-app-dashboards--show.png index d1aeb4350f6df..4aaadd263a2a6 100644 Binary files a/frontend/__snapshots__/scenes-app-dashboards--show.png and b/frontend/__snapshots__/scenes-app-dashboards--show.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png index 19c18d8c07666..153a93af4a41a 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png and b/frontend/__snapshots__/scenes-app-experiments--complete-funnel-experiment.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png index 3fd4539ca2827..ac1f227514773 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png and b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends--webkit.png index d5e21ff466ce3..04f3d65e359ee 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png index 55a8d66037312..21e554f56ab8e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png index fec89fd6ab2dc..9574d6e03d310 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends.png index 3d8b801d0f7ac..a0bef56dc2724 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png index b0e9092477022..0874beff87993 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png index 8e5c541cb9582..4b24320571880 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png index c7f7d20533d1a..abf96cf1c2c1d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png index c701498693b9a..ca136afb5c010 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png index 2609257c6b1a3..b6d6f129d5e66 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png index 62cade73f3285..cc7a5e5bc6233 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png index efe17f5e08795..afe5f32e5f79b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png index 07e5a65dc34cd..6e392b8df0d66 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert--webkit.png index 729d241639c48..2fd2ddfd0735c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png index 9a86cb9f9c545..e4de49978fb59 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png index f9d430d61bb50..ade798e8b8585 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert.png index 8267c7120b225..97187756246b5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--webkit.png index f1a9e173682f5..acc10d101122d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--webkit.png index b3c9d3a3b4ce9..607f6f853a18a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png index ead27a6b6ed58..eea650b28efaf 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png index 2d7f9f2c91a3b..6414315f95b96 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png index 70b1c0d62928a..bb468c41d7099 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png index 8b1c53c47f5ea..12e046675a97c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png index f349c2d4ebe8c..4a767b6971349 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png index 0c40f9ad52881..63cf76f8cfa5d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle--webkit.png index 16693f43e01fc..fd423f1f7e19e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle--webkit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png index 3d7921c62f64f..dd0aec35f1f09 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit.png index 4dbce40bf0e19..15c56dfa56a59 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle.png b/frontend/__snapshots__/scenes-app-insights--lifecycle.png index 8028d8f042b92..2ab70086dee61 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention--webkit.png index fb87e41b665c4..2e7368c06bb1e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown--webkit.png index 77c2f6447b18e..402402919e7ae 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--webkit.png index 258ae400390f0..385f05ea6589b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit.png index fca1587e44247..97fa118d79f56 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown.png index 2e2cf79187652..121d0b324adcd 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-edit--webkit.png index 44f67fcbe4004..c8ede7b9fd30c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-edit.png b/frontend/__snapshots__/scenes-app-insights--retention-edit.png index cdc6ffe95c0ca..ae71eec088987 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-edit.png and b/frontend/__snapshots__/scenes-app-insights--retention-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention.png b/frontend/__snapshots__/scenes-app-insights--retention.png index 091d0c0869206..e90937f466eef 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention.png and b/frontend/__snapshots__/scenes-app-insights--retention.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness--webkit.png index 56900048c8d45..eaf977559e8f2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness--webkit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--webkit.png index ab6e8cb04a5de..62e04313ddc65 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit.png index 201ac3ffde766..29db39c34e2c8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness.png b/frontend/__snapshots__/scenes-app-insights--stickiness.png index 38528945400b9..885c792dbff15 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness.png and b/frontend/__snapshots__/scenes-app-insights--stickiness.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area--webkit.png index 26ffb8ab301f7..56216f518af65 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown--webkit.png index c8dbde3f7a571..b5c8307dea666 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png index a51360f6820f2..9905ed61d7a89 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png index 5b0d3fe919d7d..18e8183ddefb1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown.png index 4ad1ee2377671..f412b05f949a1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--webkit.png index b440fd4dc1dd8..1c9bae1872eee 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit.png index a1314f53ff959..2513bc614a783 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area.png b/frontend/__snapshots__/scenes-app-insights--trends-area.png index 07a14dd5c487c..693bb117d362c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area.png and b/frontend/__snapshots__/scenes-app-insights--trends-area.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar--webkit.png index 6c4a88f6a1598..029db225e47ab 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--webkit.png index 1379e0418168a..7a047299abaef 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png index cd4d8513f726f..aa1a85fbbf26f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png index 1535db5abeb7c..42115a622a205 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown.png index 9fd5ca2cebc20..b76b6e7a17d98 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--webkit.png index 58b481326617c..adb88102026c6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit.png index 765cd6350b050..1b81ed4977ff1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar.png b/frontend/__snapshots__/scenes-app-insights--trends-bar.png index 511b168b8bf52..cb863bb9c9bb6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line--webkit.png index 42977de5e5098..7814a705e190d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--webkit.png index 61359c68e75ae..5dd79e1bb0217 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png index 1dc4f0ec8923f..2779891938055 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png index a48332e677519..370cd4a4c10a2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown.png index 41cb4446a312a..1ec537b3468ad 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--webkit.png index f9063f35a84fa..9324b2235c046 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit.png index 57362c17debe0..7bb39aaff2b5c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line.png b/frontend/__snapshots__/scenes-app-insights--trends-line.png index 18676e1d52235..b47fdcfa793e0 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line.png and b/frontend/__snapshots__/scenes-app-insights--trends-line.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-number--webkit.png index 432bd0c3bf4c5..fcf0496097bfc 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--webkit.png index 526c04b338271..2a5f905f2d2cb 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit.png index fe0bcf496b57f..25d7659b28b81 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number.png b/frontend/__snapshots__/scenes-app-insights--trends-number.png index a6348d35d2af9..c4b27d87904c5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number.png and b/frontend/__snapshots__/scenes-app-insights--trends-number.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie--webkit.png index fd48c50e52abd..d7d9d4334e44b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--webkit.png index f7e26559e417e..9926ab50ea419 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png index ca2c8c2c02b22..5163ac5fdb238 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png index 45a6e052b19a5..e020cb76a7760 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown.png index f34bec7fb925f..e396c60572415 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--webkit.png index b287382a43cd1..af5c8b64103fe 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit.png index b01e399a2b526..0a8ebe4751399 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie.png b/frontend/__snapshots__/scenes-app-insights--trends-pie.png index dcc567929263b..5f83bed8ef407 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table--webkit.png index 9c5ea39db2de7..9b85c727a05ab 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown--webkit.png index e4eff45c48f5d..90f6c74468cec 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png index 42a52ceb9abd6..4196d96dfbd67 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png index f998019bf447d..8ae256c8199bf 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown.png index 41a8d37374837..1d66d872793ef 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--webkit.png index 108f5c8c9050c..a8ef498debcd6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit.png index 2531352703251..35e186b20d96c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table.png b/frontend/__snapshots__/scenes-app-insights--trends-table.png index 66e2ff923a707..83727bad0e64a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table.png and b/frontend/__snapshots__/scenes-app-insights--trends-table.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value--webkit.png index b2fca73f9873c..a350461bf3689 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown--webkit.png index 3980597d676b3..8af97f329f0d5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png index 538155e5a40c3..c626cb8402ae2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png index 2dc48470ffb05..f45c73c45217e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown.png index a1aa07087d84e..98d682c860521 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--webkit.png index 50225163c439d..48b1f53b59bfb 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit.png index 8ac43bccc823c..bdb269d4562ee 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value.png b/frontend/__snapshots__/scenes-app-insights--trends-value.png index f1aa7ebba9539..24e25225e9902 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value.png and b/frontend/__snapshots__/scenes-app-insights--trends-value.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map--webkit.png index 865ba04c0b785..1ae8bc9fccb15 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png index b4feed5f4c9df..709a2698bae3b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png index 48bfcb56e7441..0462e9cfedd78 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map.png index c7dce61d12139..e76857963a4de 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths--webkit.png index e9041e787e53f..e990fa30da8b9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths--webkit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--webkit.png index c10fbd9a66423..25086a8cbda7f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit.png index 0ec60139a4c49..1c01944a7c95c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths.png b/frontend/__snapshots__/scenes-app-insights--user-paths.png index 2cd749ffe0568..ca4c44785108f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths.png and b/frontend/__snapshots__/scenes-app-insights--user-paths.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png b/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png new file mode 100644 index 0000000000000..00ac16d82c920 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--headings.png b/frontend/__snapshots__/scenes-app-notebooks--headings.png new file mode 100644 index 0000000000000..435b7c638c07d Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--headings.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--notebooks-template-introduction.png b/frontend/__snapshots__/scenes-app-notebooks--notebooks-template-introduction.png new file mode 100644 index 0000000000000..b6466dd921cf7 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--notebooks-template-introduction.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png b/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png new file mode 100644 index 0000000000000..76256d08a1d61 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png new file mode 100644 index 0000000000000..ae06ad761790f Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--text-formats.png b/frontend/__snapshots__/scenes-app-notebooks--text-formats.png new file mode 100644 index 0000000000000..ec6cf87e153ed Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--text-formats.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--text-only-notebook.png b/frontend/__snapshots__/scenes-app-notebooks--text-only-notebook.png index 7452eabf77180..bc2f358a8286c 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks--text-only-notebook.png and b/frontend/__snapshots__/scenes-app-notebooks--text-only-notebook.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--closed-popover-state.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--closed-popover-state.png index 7e09e95a91e3d..72044664032ff 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--closed-popover-state.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--closed-popover-state.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--default.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--default.png index 2d8bc66db9e21..ca05fd2fff918 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--default.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--default.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-existing-containing-notebooks.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-existing-containing-notebooks.png index 3c573bbf93467..17c750c0c42d7 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-existing-containing-notebooks.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-existing-containing-notebooks.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-notebooks.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-notebooks.png index 2d8bc66db9e21..ca05fd2fff918 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-notebooks.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-no-notebooks.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response-closed-popover.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response-closed-popover.png index 4c0d0f64fd16a..7f2f047e58950 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response-closed-popover.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response-closed-popover.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response.png b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response.png index cc47c9d64706c..2e25a8113f1d1 100644 Binary files a/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response.png and b/frontend/__snapshots__/scenes-app-notebooks-components-notebook-select-button--with-slow-network-response.png differ diff --git a/frontend/__snapshots__/scenes-app-recordings--recent-recordings.png b/frontend/__snapshots__/scenes-app-recordings--recent-recordings.png new file mode 100644 index 0000000000000..edcf79a87d9ad Binary files /dev/null and b/frontend/__snapshots__/scenes-app-recordings--recent-recordings.png differ diff --git a/frontend/__snapshots__/scenes-app-recordings--recordings-list.png b/frontend/__snapshots__/scenes-app-recordings--recordings-list.png index f4060d584e979..edcf79a87d9ad 100644 Binary files a/frontend/__snapshots__/scenes-app-recordings--recordings-list.png and b/frontend/__snapshots__/scenes-app-recordings--recordings-list.png differ diff --git a/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png b/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png index 8e3052db551e1..072bef08ef7e3 100644 Binary files a/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png and b/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png differ diff --git a/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-with-pinned-recordings.png b/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-with-pinned-recordings.png index fc2cd7d3326f9..5026277f808c0 100644 Binary files a/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-with-pinned-recordings.png and b/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-with-pinned-recordings.png differ diff --git a/frontend/__snapshots__/scenes-app-recordings--second-recording-in-list.png b/frontend/__snapshots__/scenes-app-recordings--second-recording-in-list.png index 5927803f6a537..afede51513048 100644 Binary files a/frontend/__snapshots__/scenes-app-recordings--second-recording-in-list.png and b/frontend/__snapshots__/scenes-app-recordings--second-recording-in-list.png differ diff --git a/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement.png b/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement.png index b533f8a57619e..e34fe137f3088 100644 Binary files a/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement.png and b/frontend/__snapshots__/scenes-other-login--cloud-with-google-login-enforcement.png differ diff --git a/frontend/src/layout/navigation/TopBar/Announcement.tsx b/frontend/src/layout/navigation/TopBar/Announcement.tsx index e5bbd1f1507a5..24b0d3e73ec6b 100644 --- a/frontend/src/layout/navigation/TopBar/Announcement.tsx +++ b/frontend/src/layout/navigation/TopBar/Announcement.tsx @@ -6,7 +6,7 @@ import { NewFeatureBanner } from 'lib/introductions/NewFeatureBanner' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { LemonButton, Link } from '@posthog/lemon-ui' import { IconClose } from 'lib/lemon-ui/icons' -import ReactMarkdown from 'react-markdown' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' window.process = MOCK_NODE_PROCESS @@ -37,7 +37,7 @@ export function Announcement(): JSX.Element | null { ) } else if (shownAnnouncementType === AnnouncementType.CloudFlag && cloudAnnouncement) { - message = {cloudAnnouncement} + message = {cloudAnnouncement} } else if (shownAnnouncementType === AnnouncementType.NewFeature) { message = } diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index cf73ee60f4b8b..4c51c2453ae40 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -30,6 +30,26 @@ export function TopBar(): JSX.Element { const { hideInviteModal } = useActions(inviteLogic) const { groupNamesTaxonomicTypes } = useValues(groupsModel) const { featureFlags } = useValues(featureFlagLogic) + + const hasNotebooks = !!featureFlags[FEATURE_FLAGS.NOTEBOOKS] + + const groupTypes = [ + TaxonomicFilterGroupType.Events, + TaxonomicFilterGroupType.Persons, + TaxonomicFilterGroupType.Actions, + TaxonomicFilterGroupType.Cohorts, + TaxonomicFilterGroupType.Insights, + TaxonomicFilterGroupType.FeatureFlags, + TaxonomicFilterGroupType.Plugins, + TaxonomicFilterGroupType.Experiments, + TaxonomicFilterGroupType.Dashboards, + ...groupNamesTaxonomicTypes, + ] + + if (hasNotebooks) { + groupTypes.push(TaxonomicFilterGroupType.Notebooks) + } + return ( <> @@ -48,26 +68,12 @@ export function TopBar(): JSX.Element {
- +
- {!!featureFlags[FEATURE_FLAGS.NOTEBOOKS] && } + {hasNotebooks && } diff --git a/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx b/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx index 3b80c5f5039ff..cc11a0c436539 100644 --- a/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx +++ b/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx @@ -7,8 +7,8 @@ import { ActivityLogItem, humanize, HumanizedActivityLogItem } from 'lib/compone import type { notificationsLogicType } from './notificationsLogicType' import { describerFor } from 'lib/components/ActivityLog/activityLogLogic' import { dayjs } from 'lib/dayjs' -import ReactMarkdown from 'react-markdown' import posthog from 'posthog-js' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' const POLL_TIMEOUT = 5 * 60 * 1000 const MARK_READ_TIMEOUT = 2500 @@ -156,11 +156,7 @@ export const notificationsLogic = kea([ email: 'joe@posthog.com', name: 'Joe', isSystem: true, - description: ( - <> - {changelogNotification.markdown} - - ), + description: {changelogNotification.markdown}, created_at: changelogNotification.notificationDate, unread: changeLogIsUnread, } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 95e72ac18729d..c56bf0bc8086e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1321,7 +1321,7 @@ const api = { }, async update( notebookId: NotebookType['short_id'], - data: Pick + data: Pick ): Promise { return await new ApiRequest().notebook(notebookId).update({ data }) }, @@ -1348,11 +1348,11 @@ const api = { q = { ...q, created_by: createdBy } } if (search) { - q = { ...q, s: search } + q = { ...q, search: search } } return await apiRequest.withQueryString(q).get() }, - async create(data?: Pick): Promise { + async create(data?: Pick): Promise { return await new ApiRequest().notebooks().create({ data }) }, async delete(notebookId: NotebookType['short_id']): Promise { diff --git a/frontend/src/lib/components/Cards/TextCard/TextCard.scss b/frontend/src/lib/components/Cards/TextCard/TextCard.scss index 0652dd7fa64bb..f88af17286e05 100644 --- a/frontend/src/lib/components/Cards/TextCard/TextCard.scss +++ b/frontend/src/lib/components/Cards/TextCard/TextCard.scss @@ -9,13 +9,13 @@ overflow-y: auto; ul { - list-style: disc; - padding-inline-start: 1.5em; + list-style-type: disc; + list-style-position: inside; } ol { - list-style: numeric; - padding-inline-start: 1.5em; + list-style-type: numeric; + list-style-position: inside; } img { diff --git a/frontend/src/lib/components/Cards/TextCard/TextCard.tsx b/frontend/src/lib/components/Cards/TextCard/TextCard.tsx index 9b2a9d8705ddf..61d6eaee91c93 100644 --- a/frontend/src/lib/components/Cards/TextCard/TextCard.tsx +++ b/frontend/src/lib/components/Cards/TextCard/TextCard.tsx @@ -6,10 +6,10 @@ import { LemonButton, LemonButtonWithDropdown, LemonDivider } from '@posthog/lem import { useActions, useValues } from 'kea' import { router } from 'kea-router' import { urls } from 'scenes/urls' -import ReactMarkdown from 'react-markdown' import { dashboardsModel } from '~/models/dashboardsModel' import React, { useState } from 'react' import { CardMeta, Resizeable } from 'lib/components/Cards/CardMeta' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' interface TextCardProps extends React.HTMLAttributes, Resizeable { dashboardId?: string | number @@ -24,16 +24,16 @@ interface TextCardProps extends React.HTMLAttributes, Resizeable showEditingControls?: boolean } -interface TextCardBodyProps extends Pick, 'style'> { +interface TextCardBodyProps extends Pick, 'style' | 'className'> { text: string closeDetails?: () => void } -export function TextContent({ text, closeDetails, style }: TextCardBodyProps): JSX.Element { +export function TextContent({ text, closeDetails, style, className }: TextCardBodyProps): JSX.Element { return ( // eslint-disable-next-line react/forbid-dom-props -
closeDetails?.()} style={style}> - {text} +
closeDetails?.()} style={style}> + {text}
) } diff --git a/frontend/src/lib/components/Cards/TextCard/TextCardModal.tsx b/frontend/src/lib/components/Cards/TextCard/TextCardModal.tsx index f1e9c7a9f46ad..a29655f96afba 100644 --- a/frontend/src/lib/components/Cards/TextCard/TextCardModal.tsx +++ b/frontend/src/lib/components/Cards/TextCard/TextCardModal.tsx @@ -4,7 +4,7 @@ import { useActions, useValues } from 'kea' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { Field, Form } from 'kea-forms' -import { LemonTextMarkdown } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' +import { LemonTextAreaMarkdown } from 'lib/lemon-ui/LemonTextArea/LemonTextArea' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' import { userLogic } from 'scenes/userLogic' @@ -71,7 +71,7 @@ export function TextCardModal({ > - + diff --git a/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss b/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss index 87a256ecda539..f7d40aaf1a9b5 100644 --- a/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss +++ b/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss @@ -1,21 +1,21 @@ .CodeSnippet { position: relative; + font-size: 0.875rem; + &.CodeSnippet--compact { + font-size: 0.8125rem; + .CodeSnippet__actions { + top: 0.375rem; + right: 0.375rem; + } + } .CodeSnippet__actions { position: absolute; display: flex; top: 0.25rem; right: 0.25rem; gap: 0.5rem; - font-size: 1.5rem; - - // NOTE: This is not ideal as we should not override core components styles but... - .LemonButton { - background: transparent !important; - box-shadow: transparent !important; - - .LemonIcon { - color: #fff; - } + .LemonButton .LemonIcon { + color: #fff; } } } diff --git a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx index 3af750522f305..180ac9e794838 100644 --- a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx +++ b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx @@ -28,6 +28,7 @@ import { useValues } from 'kea' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' import { useState } from 'react' +import clsx from 'clsx' export enum Language { Text = 'text', @@ -83,6 +84,7 @@ export interface CodeSnippetProps { children: string language?: Language wrap?: boolean + compact?: boolean actions?: Action[] style?: React.CSSProperties /** What is being copied. @example 'link' */ @@ -95,6 +97,7 @@ export function CodeSnippet({ children: text, language = Language.Text, wrap = false, + compact = false, style, actions, thing = 'snippet', @@ -109,15 +112,20 @@ export function CodeSnippet({ return ( // eslint-disable-next-line react/forbid-dom-props -
+
{actions && actions.map(({ icon, callback, popconfirmProps, title }, index) => !popconfirmProps ? ( - + ) : ( - + ) )} @@ -127,6 +135,7 @@ export function CodeSnippet({ onClick={async () => { text && (await copyToClipboard(text, thing)) }} + size={compact ? 'small' : 'medium'} />
= { title: 'Components/Editable Field', component: EditableFieldComponent, + tags: ['autodocs'], } export default meta -export function EditableField_(): JSX.Element { - const [savedTitle, setSavedTitle] = useState('Foo') - const [savedDescription, setSavedDescription] = useState('Lorem ipsum dolor sit amet.') +const Template: StoryFn = (args) => { + const [value, setValue] = useState(args.value ?? 'Lorem ipsum') return ( - setSavedTitle(value)} />} - caption={ - setSavedDescription(value)} - multiline - /> - } - /> +
+ setValue(value)} /> +
) } + +export const Default = Template.bind({}) + +export const MultilineWithMarkdown = Template.bind({}) +MultilineWithMarkdown.args = { + multiline: true, + markdown: true, + value: 'Lorem ipsum **dolor** sit amet, consectetur adipiscing _elit_.', +} diff --git a/frontend/src/lib/components/EditableField/EditableField.tsx b/frontend/src/lib/components/EditableField/EditableField.tsx index 3360c872f608d..f30517c7e08d1 100644 --- a/frontend/src/lib/components/EditableField/EditableField.tsx +++ b/frontend/src/lib/components/EditableField/EditableField.tsx @@ -1,13 +1,14 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import './EditableField.scss' -import { IconEdit } from 'lib/lemon-ui/icons' +import { IconEdit, IconMarkdown } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import TextareaAutosize from 'react-textarea-autosize' import clsx from 'clsx' import { pluralize } from 'lib/utils' import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' -interface EditableFieldProps { +export interface EditableFieldProps { /** What this field stands for. */ name: string value: string @@ -19,6 +20,8 @@ interface EditableFieldProps { maxLength?: number autoFocus?: boolean multiline?: boolean + /** Whether to render the content as Markdown in view mode. */ + markdown?: boolean compactButtons?: boolean /** Whether this field should be gated behind a "paywall". */ paywall?: boolean @@ -46,6 +49,7 @@ export function EditableField({ maxLength, autoFocus = true, multiline = false, + markdown = false, compactButtons = false, paywall = false, mode, @@ -116,7 +120,7 @@ export function EditableField({ : undefined } > -
+
{isEditing ? ( <> {multiline ? ( @@ -151,7 +155,12 @@ export function EditableField({ /> )} {!mode && ( - <> +
+ {markdown && ( + + + + )} {saveButtonText} - +
)} ) : ( <> - {tentativeValue || {placeholder}} + {tentativeValue && markdown ? ( + {tentativeValue} + ) : ( + tentativeValue || {placeholder} + )} {!mode && ( - } - size={compactButtons ? 'small' : undefined} - onClick={() => setLocalIsEditing(true)} - data-attr={`edit-prop-${name}`} - disabled={paywall} - noPadding - /> +
+ } + size={compactButtons ? 'small' : undefined} + onClick={() => setLocalIsEditing(true)} + data-attr={`edit-prop-${name}`} + disabled={paywall} + noPadding + /> +
)} )} diff --git a/frontend/src/lib/components/SignupReferralSource.tsx b/frontend/src/lib/components/SignupReferralSource.tsx new file mode 100644 index 0000000000000..c56ff9931b7ff --- /dev/null +++ b/frontend/src/lib/components/SignupReferralSource.tsx @@ -0,0 +1,15 @@ +import { LemonInput } from '@posthog/lemon-ui' +import { Field } from 'lib/forms/Field' + +export default function SignupReferralSource({ disabled }: { disabled: boolean }): JSX.Element { + return ( + + + + ) +} diff --git a/frontend/src/lib/components/SignupReferralSourceSelect.tsx b/frontend/src/lib/components/SignupReferralSourceSelect.tsx deleted file mode 100644 index 13d58b2de9caf..0000000000000 --- a/frontend/src/lib/components/SignupReferralSourceSelect.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Field } from 'lib/forms/Field' -import { LemonSelect } from 'lib/lemon-ui/LemonSelect' - -export default function SignupReferralSourceSelect({ className }: { className?: string }): JSX.Element { - return ( - - - - ) -} diff --git a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx index 2550bcff4d546..7034d975c324b 100644 --- a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx +++ b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx @@ -285,7 +285,7 @@ export function EditSubscription({ > {({ value, onChange }) => ( onChange(val)} + onChange={(val: string) => onChange(val)} value={value} disabled={slackDisabled} mode="single" diff --git a/frontend/src/lib/components/Support/supportLogic.ts b/frontend/src/lib/components/Support/supportLogic.ts index 9f36682b56d53..879f1ff53d729 100644 --- a/frontend/src/lib/components/Support/supportLogic.ts +++ b/frontend/src/lib/components/Support/supportLogic.ts @@ -55,6 +55,7 @@ export const TARGET_AREA_TO_NAME = { feature_flags: 'Feature Flags', analytics: 'Product Analytics (Insights, Dashboards, Annotations)', session_replay: 'Session Replay (Recordings)', + toolbar: 'Toolbar & heatmaps', surveys: 'Surveys', } @@ -81,7 +82,7 @@ export const URL_PATH_TO_TARGET_AREA: Record = persons: 'data_integrity', groups: 'data_integrity', app: 'apps', - toolbar: 'analytics', + toolbar: 'session_replay', warehouse: 'data_warehouse', surveys: 'surveys', } diff --git a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx index 773cfa0809949..e9014b314ef91 100644 --- a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx @@ -23,6 +23,7 @@ import { PersonType, PluginType, PropertyDefinition, + NotebookType, } from '~/types' import { cohortsModel } from '~/models/cohortsModel' import { actionsModel } from '~/models/actionsModel' @@ -154,7 +155,7 @@ export const taxonomicFilterLogic = kea({ eventNames, excludedProperties ): TaxonomicFilterGroup[] => { - return [ + const groups = [ { name: 'Events', searchPlaceholder: 'events', @@ -209,7 +210,7 @@ export const taxonomicFilterLogic = kea({ filter_by_event_names: true, }).url : undefined, - expandLabel: ({ count, expandedCount }) => + expandLabel: ({ count, expandedCount }: { count: number; expandedCount: number }) => `Show ${pluralize(expandedCount - count, 'property', 'properties')} that ${pluralize( eventNames.length, 'has', @@ -237,7 +238,7 @@ export const taxonomicFilterLogic = kea({ filter_by_event_names: true, }).url : undefined, - expandLabel: ({ count, expandedCount }) => + expandLabel: ({ count, expandedCount }: { count: number; expandedCount: number }) => `Show ${pluralize(expandedCount - count, 'property', 'properties')} that ${pluralize( eventNames.length, 'has', @@ -398,6 +399,16 @@ export const taxonomicFilterLogic = kea({ getValue: (dashboard: DashboardType) => dashboard.id, getPopoverHeader: () => `Dashboards`, }, + { + name: 'Notebooks', + searchPlaceholder: 'notebooks', + type: TaxonomicFilterGroupType.Notebooks, + value: 'notebooks', + endpoint: `api/projects/${teamId}/notebooks/`, + getName: (notebook: NotebookType) => notebook.title || `Notebook ${notebook.short_id}`, + getValue: (notebook: NotebookType) => notebook.short_id, + getPopoverHeader: () => 'Notebooks', + }, { name: 'Sessions', searchPlaceholder: 'sessions', @@ -408,8 +419,8 @@ export const taxonomicFilterLogic = kea({ value: '$session_duration', }, ], - getName: (option) => option.name, - getValue: (option) => option.value, + getName: (option: any) => option.name, + getValue: (option: any) => option.value, getPopoverHeader: () => 'Session', }, { @@ -422,6 +433,8 @@ export const taxonomicFilterLogic = kea({ ...groupAnalyticsTaxonomicGroups, ...groupAnalyticsTaxonomicGroupNames, ] + + return groups }, ], activeTaxonomicGroup: [ diff --git a/frontend/src/lib/components/TaxonomicFilter/types.ts b/frontend/src/lib/components/TaxonomicFilter/types.ts index 5d03149f671ea..5dd74ef575aae 100644 --- a/frontend/src/lib/components/TaxonomicFilter/types.ts +++ b/frontend/src/lib/components/TaxonomicFilter/types.ts @@ -83,6 +83,7 @@ export enum TaxonomicFilterGroupType { GroupNamesPrefix = 'name_groups', Sessions = 'sessions', HogQLExpression = 'hogql_expression', + Notebooks = 'notebooks', } export interface InfiniteListLogicProps extends TaxonomicFilterLogicProps { diff --git a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopover.tsx b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopover.tsx index 2412e0b8bff8d..dc8e9384a7fd5 100644 --- a/frontend/src/lib/components/UniversalSearch/UniversalSearchPopover.tsx +++ b/frontend/src/lib/components/UniversalSearch/UniversalSearchPopover.tsx @@ -109,6 +109,8 @@ function redirectOnSelectItems( ) } else if (groupType === TaxonomicFilterGroupType.Dashboards) { router.actions.push(urls.dashboard(value)) + } else if (groupType === TaxonomicFilterGroupType.Notebooks) { + router.actions.push(urls.notebook(String(value))) } } diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index c0d87142bfd6d..1842e4f2adb4f 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -155,16 +155,17 @@ export const FEATURE_FLAGS = { FF_DASHBOARD_TEMPLATES: 'ff-dashboard-templates', // owner: @EDsCODE SHOW_PRODUCT_INTRO_EXISTING_PRODUCTS: 'show-product-intro-existing-products', // owner: @raquelmsmith ARTIFICIAL_HOG: 'artificial-hog', // owner: @Twixes - REFERRAL_SOURCE_SELECT: 'referral-source-select', // owner: @raquelmsmith SURVEYS_MULTIPLE_CHOICE: 'surveys-multiple-choice', // owner: @liyiy CS_DASHBOARDS: 'cs-dashboards', // owner: @pauldambra - NOTEBOOK_SETTINGS_WIDGETS: 'notebook-settings-widgets', // owner: #team-monitoring PRODUCT_SPECIFIC_ONBOARDING: 'product-specific-onboarding', // owner: @raquelmsmith REDIRECT_SIGNUPS_TO_INSTANCE: 'redirect-signups-to-instance', // owner: @raquelmsmith APPS_AND_EXPORTS_UI: 'apps-and-exports-ui', // owner: @benjackwhite SURVEY_NPS_RESULTS: 'survey-nps-results', // owner: @liyiy // owner: #team-monitoring SESSION_RECORDING_ALLOW_V1_SNAPSHOTS: 'session-recording-allow-v1-snapshots', + SESSION_REPLAY_CORS_PROXY: 'session-replay-cors-proxy', // owner: #team-monitoring + HOGQL_INSIGHTS: 'hogql-insights', // owner: @mariusandra + WEBHOOKS_DENYLIST: 'webhooks-denylist', // owner: #team-pipeline } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx index c60a2fd9eb933..25cf4daf6e04d 100644 --- a/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx +++ b/frontend/src/lib/lemon-ui/LemonFileInput/LemonFileInput.tsx @@ -136,13 +136,15 @@ export const LemonFileInput = ({ Click or drag and drop to upload {accept ? ` ${acceptToDisplayName(accept)}` : ''} -
- {files.map((x, i) => ( - : undefined}> - {x.name} - - ))} -
+ {files.length > 0 && ( +
+ {files.map((x, i) => ( + : undefined}> + {x.name} + + ))} +
+ )}
) diff --git a/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.scss b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.scss new file mode 100644 index 0000000000000..1858e38e78e92 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.scss @@ -0,0 +1,30 @@ +.LemonMarkdown { + > * { + margin: 0 0 0.5em 0; + &:last-child { + margin-bottom: 0; + } + } + ol, + ul, + dl { + padding-left: 1.5em; + } + ol { + list-style-type: decimal; + } + ul { + list-style-type: disc; + } + strong[level] { + // Low-key headings + display: block; + } + hr { + margin: 1em 0; + } + h1 { + margin-bottom: 0.25em; + font-weight: 600; + } +} diff --git a/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.stories.tsx b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.stories.tsx new file mode 100644 index 0000000000000..89c4b786360e6 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.stories.tsx @@ -0,0 +1,46 @@ +import { Meta, StoryFn, StoryObj } from '@storybook/react' +import { LemonMarkdown as LemonMarkdownComponent, LemonMarkdownProps } from './LemonMarkdown' + +type Story = StoryObj +const meta: Meta = { + title: 'Lemon UI/Lemon Markdown', + component: LemonMarkdownComponent, + tags: ['autodocs'], +} +export default meta + +const Template: StoryFn = (props: LemonMarkdownProps) => { + return +} + +export const Default: Story = Template.bind({}) +Default.args = { + children: `# Lorem ipsum + +## Linguae despexitque sine sua tibi + +Lorem markdownum et, dant officio siquid indigenae. Spectatrix contigit tellus, sum [summos](http://sim.org/sit), suis. + +- Quattuor creditur +- Veniebat patriaeque cavatur +- En anguem tamen + +\`\`\`python +print("X") +\`\`\` + +--- + +1. Quattuor creditur +2. Veniebat patriaeque cavatu +3. En anguem tamen`, +} + +export const LowKeyHeadings: Story = Template.bind({}) +LowKeyHeadings.args = { + children: `# Level 1 +## Level 2 + +**Strong** and *emphasized* text.`, + lowKeyHeadings: true, +} diff --git a/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.tsx b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.tsx new file mode 100644 index 0000000000000..df3215e2831b4 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.tsx @@ -0,0 +1,42 @@ +import ReactMarkdown from 'react-markdown' +import './LemonMarkdown.scss' +import { Link } from '../Link' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import clsx from 'clsx' + +export interface LemonMarkdownProps { + children: string + /** Whether headings should just be text. Recommended for item descriptions. */ + lowKeyHeadings?: boolean + className?: string +} + +/** Beautifully rendered Markdown. */ +export function LemonMarkdown({ children, lowKeyHeadings = false, className }: LemonMarkdownProps): JSX.Element { + return ( +
+ ( + + {children} + + ), + code: ({ language, value }) => ( + + {value} + + ), + ...(lowKeyHeadings + ? { + heading: 'strong', + } + : {}), + }} + disallowedTypes={['html']} // Don't want to deal with the security considerations of HTML + > + {children} + +
+ ) +} diff --git a/frontend/src/lib/lemon-ui/LemonMarkdown/index.ts b/frontend/src/lib/lemon-ui/LemonMarkdown/index.ts new file mode 100644 index 0000000000000..0ed0a63f5e870 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonMarkdown/index.ts @@ -0,0 +1 @@ +export { LemonMarkdown, type LemonMarkdownProps } from './LemonMarkdown' diff --git a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx index 4bec9b6b1002d..49bee6c0f3589 100644 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx +++ b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx @@ -18,20 +18,20 @@ export interface LemonSelectMultipleOptionItem extends LemonSelectMultipleOption export type LemonSelectMultipleOptions = Record -export interface LemonSelectMultipleProps { +export type LemonSelectMultipleProps = { selectClassName?: string options?: LemonSelectMultipleOptions | LemonSelectMultipleOptionItem[] - value?: string[] | null | LabelInValue[] + value?: string | string[] | null disabled?: boolean loading?: boolean placeholder?: string labelInValue?: boolean - onChange?: ((newValue: string[]) => void) | ((newValue: LabelInValue[]) => void) onSearch?: (value: string) => void onFocus?: () => void onBlur?: () => void filterOption?: boolean mode?: 'single' | 'multiple' | 'multiple-custom' + onChange?: ((newValue: string) => void) | ((newValue: string[]) => void) 'data-attr'?: string } @@ -82,9 +82,10 @@ export function LemonSelectMultiple({ showAction={['focus']} onChange={(v) => { if (onChange) { - if (labelInValue) { - const typedValues = v as LabelInValue[] - const typedOnChange = onChange as (newValue: LabelInValue[]) => void + // TRICKY: V is typed poorly and will be a string if the "mode" is undefined + if (!v || typeof v === 'string') { + const typedValues = v as string | null + const typedOnChange = onChange as (newValue: string | null) => void typedOnChange(typedValues) } else { const typedValues = v.map((token) => token.toString().trim()) as string[] diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss index d15a0b710a0d5..389975e57915a 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss @@ -34,3 +34,15 @@ border: 1px solid var(--danger); } } + +.LemonTextArea--preview { + ul { + list-style-type: disc; + list-style-position: inside; + } + + ol { + list-style-type: decimal; + list-style-position: inside; + } +} diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.stories.tsx b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.stories.tsx index 842e900a6262c..ea0f7ee62571a 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.stories.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { Meta, StoryFn, StoryObj } from '@storybook/react' -import { LemonTextArea, LemonTextAreaProps, LemonTextMarkdown as _LemonTextMarkdown } from './LemonTextArea' +import { LemonTextArea, LemonTextAreaProps, LemonTextAreaMarkdown as _LemonTextMarkdown } from './LemonTextArea' type Story = StoryObj const meta: Meta = { diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx index 9a85e20ba4829..80544bf071426 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx @@ -62,14 +62,14 @@ export const LemonTextArea = React.forwardRef void placeholder?: string 'data-attr'?: string } -export function LemonTextMarkdown({ value, onChange, ...editAreaProps }: LemonTextMarkdownProps): JSX.Element { +export function LemonTextAreaMarkdown({ value, onChange, ...editAreaProps }: LemonTextAreaMarkdownProps): JSX.Element { const { objectStorageAvailable } = useValues(preflightLogic) const [isPreviewShown, setIsPreviewShown] = useState(false) @@ -138,7 +138,11 @@ export function LemonTextMarkdown({ value, onChange, ...editAreaProps }: LemonTe { key: 'preview', label: 'Preview', - content: value ? : Nothing to preview, + content: value ? ( + + ) : ( + Nothing to preview + ), }, ]} /> diff --git a/frontend/src/lib/lemon-ui/LemonWidget/LemonWidget.tsx b/frontend/src/lib/lemon-ui/LemonWidget/LemonWidget.tsx index ca3f49fbfce30..36ef211c3109a 100644 --- a/frontend/src/lib/lemon-ui/LemonWidget/LemonWidget.tsx +++ b/frontend/src/lib/lemon-ui/LemonWidget/LemonWidget.tsx @@ -34,7 +34,7 @@ export function LemonWidget({ title, collapsible = true, onClose, children }: Le /> ) : ( - {title} + {title} )} {onClose && } />} diff --git a/frontend/src/lib/lemon-ui/Link/Link.scss b/frontend/src/lib/lemon-ui/Link/Link.scss index 8bc3dcca89708..556b5b293a68b 100644 --- a/frontend/src/lib/lemon-ui/Link/Link.scss +++ b/frontend/src/lib/lemon-ui/Link/Link.scss @@ -22,6 +22,6 @@ } > .LemonIcon { - margin-left: 0.3em; + margin-left: 0.15em; } } diff --git a/frontend/src/lib/lemon-ui/Link/Link.tsx b/frontend/src/lib/lemon-ui/Link/Link.tsx index 92dea9fac774a..1eeb3d487e6d2 100644 --- a/frontend/src/lib/lemon-ui/Link/Link.tsx +++ b/frontend/src/lib/lemon-ui/Link/Link.tsx @@ -25,6 +25,11 @@ export type LinkProps = Pick, 'target' | 'cla disabled?: boolean /** Like plain `disabled`, except we enforce a reason to be shown in the tooltip. */ disabledReason?: string | null | false + /** + * Whether an "open in new" icon should be shown if target is `_blank`. + * This is true by default if `children` is a string. + */ + targetBlankIcon?: boolean } // Some URLs we want to enforce a full reload such as billing which is redirected by Django @@ -56,6 +61,7 @@ export const Link: React.FC> = Reac children, disabled, disabledReason, + targetBlankIcon = typeof children === 'string', ...props }, ref @@ -101,7 +107,7 @@ export const Link: React.FC> = Reac {...draggableProps} > {children} - {typeof children === 'string' && target === '_blank' ? : null} + {targetBlankIcon && target === '_blank' ? : null} ) : ( [200, []], + 'https://www.gravatar.com/avatar/:gravatar_id': () => [404, ''], + 'https://app.posthog.com/api/early_access_features': { + earlyAccessFeatures: [], + }, }, post: { 'https://app.posthog.com/e/': (): MockSignature => [200, 'ok'], diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index d81b615a5adde..5687f2b424205 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -371,7 +371,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults }
{showHogQLEditor && isHogQLQuery(query.source) && !isReadOnly ? ( - + ) : null} {showFirstRow && (
diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index 1de315c2c900e..db3a26d62aba0 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -1,12 +1,17 @@ +import Papa from 'papaparse' import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' import { IconExport } from 'lib/lemon-ui/icons' import { triggerExport } from 'lib/components/ExportButton/exporter' import { ExporterFormat } from '~/types' import { DataNode, DataTableNode } from '~/queries/schema' -import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' -import { isEventsQuery, isPersonsNode } from '~/queries/utils' +import { defaultDataTableColumns, extractExpressionComment } from '~/queries/nodes/DataTable/utils' +import { isEventsQuery, isHogQLQuery, isPersonsNode } from '~/queries/utils' import { getPersonsEndpoint } from '~/queries/query' import { ExportWithConfirmation } from '~/queries/nodes/DataTable/ExportWithConfirmation' +import { DataTableRow, dataTableLogic } from './dataTableLogic' +import { useValues } from 'kea' +import { LemonDivider, lemonToast } from '@posthog/lemon-ui' +import { asDisplay } from 'scenes/persons/person-utils' const EXPORT_MAX_LIMIT = 10000 @@ -39,18 +44,148 @@ function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void }) } +const columnDisallowList = ['person.$delete', '*'] +const getCsvTableData = (dataTableRows: DataTableRow[], columns: string[], query: DataTableNode): string[][] => { + if (isPersonsNode(query.source)) { + const filteredColumns = columns.filter((n) => !columnDisallowList.includes(n)) + + const csvData = dataTableRows.map((n) => { + const record = n.result as Record | undefined + const recordWithPerson = { ...(record ?? {}), person: record?.name } + + return filteredColumns.map((n) => recordWithPerson[n]) + }) + + return [filteredColumns, ...csvData] + } + + if (isEventsQuery(query.source)) { + const filteredColumns = columns + .filter((n) => !columnDisallowList.includes(n)) + .map((n) => extractExpressionComment(n)) + + const csvData = dataTableRows.map((n) => { + return columns + .map((col, colIndex) => { + if (columnDisallowList.includes(col)) { + return null + } + + if (col === 'person') { + return asDisplay(n.result?.[colIndex]) + } + + return n.result?.[colIndex] + }) + .filter(Boolean) + }) + + return [filteredColumns, ...csvData] + } + + if (isHogQLQuery(query.source)) { + return [columns, ...dataTableRows.map((n) => (n.result as any[]) ?? [])] + } + + return [] +} + +const getJsonTableData = ( + dataTableRows: DataTableRow[], + columns: string[], + query: DataTableNode +): Record[] => { + if (isPersonsNode(query.source)) { + const filteredColumns = columns.filter((n) => !columnDisallowList.includes(n)) + + return dataTableRows.map((n) => { + const record = n.result as Record | undefined + const recordWithPerson = { ...(record ?? {}), person: record?.name } + + return filteredColumns.reduce((acc, cur) => { + acc[cur] = recordWithPerson[cur] + return acc + }, {} as Record) + }) + } + + if (isEventsQuery(query.source)) { + return dataTableRows.map((n) => { + return columns.reduce((acc, col, colIndex) => { + if (columnDisallowList.includes(col)) { + return acc + } + + if (col === 'person') { + acc[col] = asDisplay(n.result?.[colIndex]) + return acc + } + + const colName = extractExpressionComment(col) + + acc[colName] = n.result?.[colIndex] + + return acc + }, {} as Record) + }) + } + + if (isHogQLQuery(query.source)) { + return dataTableRows.map((n) => { + const data = n.result ?? {} + return columns.reduce((acc, cur, index) => { + acc[cur] = data[index] + return acc + }, {} as Record) + }) + } + + return [] +} + +function copyTableToCsv(dataTableRows: DataTableRow[], columns: string[], query: DataTableNode): void { + try { + const tableData = getCsvTableData(dataTableRows, columns, query) + + const csv = Papa.unparse(tableData) + + navigator.clipboard.writeText(csv).then(() => { + lemonToast.success('Table copied to clipboard!') + }) + } catch { + lemonToast.error('Copy failed!') + } +} + +function copyTableToJson(dataTableRows: DataTableRow[], columns: string[], query: DataTableNode): void { + try { + const tableData = getJsonTableData(dataTableRows, columns, query) + + const json = JSON.stringify(tableData, null, 4) + + navigator.clipboard.writeText(json).then(() => { + lemonToast.success('Table copied to clipboard!') + }) + } catch { + lemonToast.error('Copy failed!') + } +} + interface DataTableExportProps { query: DataTableNode setQuery?: (query: DataTableNode) => void } export function DataTableExport({ query }: DataTableExportProps): JSX.Element | null { + const { dataTableRows, columnsInResponse, columnsInQuery, queryWithDefaults } = useValues(dataTableLogic) + const source: DataNode = query.source const filterCount = (isEventsQuery(source) || isPersonsNode(source) ? source.properties?.length || 0 : 0) + (isEventsQuery(source) && source.event ? 1 : 0) + (isPersonsNode(source) && source.search ? 1 : 0) const canExportAllColumns = isEventsQuery(source) || isPersonsNode(source) + const showExportClipboardButtons = isPersonsNode(source) || isEventsQuery(source) || isHogQLQuery(source) return ( , - ].concat( - canExportAllColumns - ? [ - startDownload(query, false)} - actor={isPersonsNode(query.source) ? 'persons' : 'events'} - limit={EXPORT_MAX_LIMIT} - > - - Export all columns - - , - ] - : [] - ), + ] + .concat( + canExportAllColumns + ? [ + startDownload(query, false)} + actor={isPersonsNode(query.source) ? 'persons' : 'events'} + limit={EXPORT_MAX_LIMIT} + > + + Export all columns + + , + ] + : [] + ) + .concat( + showExportClipboardButtons + ? [ + , + { + if (dataTableRows) { + copyTableToCsv( + dataTableRows, + columnsInResponse ?? columnsInQuery, + queryWithDefaults + ) + } + }} + > + Copy CSV to clipboard + , + { + if (dataTableRows) { + copyTableToJson( + dataTableRows, + columnsInResponse ?? columnsInQuery, + queryWithDefaults + ) + } + }} + > + Copy JSON to clipboard + , + ] + : [] + ), }} type="secondary" icon={} diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index cdffb15567877..5fb75476e0af3 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -162,8 +162,7 @@ export const dataTableLogic = kea([ showReload: query.showReload ?? showIfFull, showTimings: query.showTimings ?? flagQueryTimingsEnabled, showElapsedTime: - query.showTimings || - flagQueryTimingsEnabled || + (query.showTimings ?? flagQueryTimingsEnabled) || (query.showElapsedTime ?? ((flagQueryRunningTimeEnabled || source.kind === NodeKind.HogQLQuery) && showIfFull)), showColumnConfigurator: query.showColumnConfigurator ?? showIfFull, diff --git a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx index ffd32f38a2904..872cf62478bcb 100644 --- a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx +++ b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx @@ -13,10 +13,12 @@ import { FEATURE_FLAGS } from 'lib/constants' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { CodeEditor } from 'lib/components/CodeEditors' +import clsx from 'clsx' export interface HogQLQueryEditorProps { query: HogQLQuery setQuery?: (query: HogQLQuery) => void + embedded?: boolean } let uniqueNode = 0 @@ -45,8 +47,7 @@ export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element {
@@ -115,87 +116,90 @@ export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element { }} /> - setQueryInput(v ?? '')} - height="100%" - onMount={(editor, monaco) => { - monaco.languages.registerCodeActionProvider('mysql', { - provideCodeActions: (model, _range, context) => { - if (logic.isMounted()) { - // Monaco gives us a list of markers that we're looking at, but without the quick fixes. - const markersFromMonaco = context.markers - // We have a list of _all_ markers returned from the HogQL metadata query - const markersFromMetadata = logic.values.modelMarkers - // We need to merge the two lists - const quickFixes: languages.CodeAction[] = [] + {/* eslint-disable-next-line react/forbid-dom-props */} +
+ setQueryInput(v ?? '')} + height="100%" + onMount={(editor, monaco) => { + monaco.languages.registerCodeActionProvider('mysql', { + provideCodeActions: (model, _range, context) => { + if (logic.isMounted()) { + // Monaco gives us a list of markers that we're looking at, but without the quick fixes. + const markersFromMonaco = context.markers + // We have a list of _all_ markers returned from the HogQL metadata query + const markersFromMetadata = logic.values.modelMarkers + // We need to merge the two lists + const quickFixes: languages.CodeAction[] = [] - for (const activeMarker of markersFromMonaco) { - const start = model.getOffsetAt({ - column: activeMarker.startColumn, - lineNumber: activeMarker.startLineNumber, - }) - const end = model.getOffsetAt({ - column: activeMarker.endColumn, - lineNumber: activeMarker.endLineNumber, - }) - for (const rawMarker of markersFromMetadata) { - if ( - rawMarker.hogQLFix && - // if ranges overlap - rawMarker.start <= end && - rawMarker.end >= start - ) { - quickFixes.push({ - title: `Replace with: ${rawMarker.hogQLFix}`, - diagnostics: [rawMarker], - kind: 'quickfix', - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: rawMarker, - text: rawMarker.hogQLFix, + for (const activeMarker of markersFromMonaco) { + const start = model.getOffsetAt({ + column: activeMarker.startColumn, + lineNumber: activeMarker.startLineNumber, + }) + const end = model.getOffsetAt({ + column: activeMarker.endColumn, + lineNumber: activeMarker.endLineNumber, + }) + for (const rawMarker of markersFromMetadata) { + if ( + rawMarker.hogQLFix && + // if ranges overlap + rawMarker.start <= end && + rawMarker.end >= start + ) { + quickFixes.push({ + title: `Replace with: ${rawMarker.hogQLFix}`, + diagnostics: [rawMarker], + kind: 'quickfix', + edit: { + edits: [ + { + resource: model.uri, + textEdit: { + range: rawMarker, + text: rawMarker.hogQLFix, + }, + versionId: undefined, }, - versionId: undefined, - }, - ], - }, - isPreferred: true, - }) + ], + }, + isPreferred: true, + }) + } } } + return { + actions: quickFixes, + dispose: () => {}, + } } - return { - actions: quickFixes, - dispose: () => {}, - } - } - }, - }) - monacoDisposables.current.push( - editor.addAction({ - id: 'saveAndRunPostHog', - label: 'Save and run query', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], - run: () => saveQuery(), + }, }) - ) - setMonacoAndEditor([monaco, editor]) - }} - options={{ - minimap: { - enabled: false, - }, - wordWrap: 'on', - scrollBeyondLastLine: false, - automaticLayout: true, - fixedOverflowWidgets: true, - }} - /> + monacoDisposables.current.push( + editor.addAction({ + id: 'saveAndRunPostHog', + label: 'Save and run query', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], + run: () => saveQuery(), + }) + ) + setMonacoAndEditor([monaco, editor]) + }} + options={{ + minimap: { + enabled: false, + }, + wordWrap: 'on', + scrollBeyondLastLine: false, + automaticLayout: true, + fixedOverflowWidgets: true, + }} + /> +
diff --git a/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx b/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx index 7a811604eda69..d9f13e313dc73 100644 --- a/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx +++ b/frontend/src/queries/nodes/InsightViz/EditorFilterGroup.tsx @@ -14,15 +14,9 @@ export interface EditorFilterGroupProps { insight: Partial insightProps: InsightLogicProps query: InsightQueryNode - setQuery: (node: InsightQueryNode) => void } -export function EditorFilterGroup({ - query, - setQuery, - insightProps, - editorFilterGroup, -}: EditorFilterGroupProps): JSX.Element { +export function EditorFilterGroup({ query, insightProps, editorFilterGroup }: EditorFilterGroupProps): JSX.Element { const { title, count, defaultExpanded = true, editorFilters } = editorFilterGroup const [isRowExpanded, setIsRowExpanded] = useState(defaultExpanded) @@ -58,7 +52,7 @@ export function EditorFilterGroup({ +
) diff --git a/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx b/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx index 1c5e57a015572..74a67460064e2 100644 --- a/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx +++ b/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx @@ -42,12 +42,11 @@ import { PathsHogQL } from 'scenes/insights/EditorFilters/PathsHogQL' export interface EditorFiltersProps { query: InsightQueryNode - setQuery: (node: InsightQueryNode) => void showing: boolean embedded: boolean } -export function EditorFilters({ query, setQuery, showing, embedded }: EditorFiltersProps): JSX.Element { +export function EditorFilters({ query, showing, embedded }: EditorFiltersProps): JSX.Element { const { user } = useValues(userLogic) const availableFeatures = user?.organization?.available_features || [] @@ -280,7 +279,6 @@ export function EditorFilters({ query, setQuery, showing, embedded }: EditorFilt insight={insight} insightProps={insightProps} query={query} - setQuery={setQuery} /> ))}
diff --git a/frontend/src/queries/nodes/InsightViz/GlobalAndOrFilters.tsx b/frontend/src/queries/nodes/InsightViz/GlobalAndOrFilters.tsx index 393bf41f04178..43998ef6f7b8d 100644 --- a/frontend/src/queries/nodes/InsightViz/GlobalAndOrFilters.tsx +++ b/frontend/src/queries/nodes/InsightViz/GlobalAndOrFilters.tsx @@ -1,20 +1,21 @@ import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { PropertyGroupFilters } from './PropertyGroupFilters/PropertyGroupFilters' -import { useValues } from 'kea' +import { useActions, useValues } from 'kea' import { groupsModel } from '~/models/groupsModel' import { TrendsQuery, StickinessQuery } from '~/queries/schema' import { isTrendsQuery } from '~/queries/utils' import { actionsModel } from '~/models/actionsModel' import { getAllEventNames } from './utils' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' type GlobalAndOrFiltersProps = { query: TrendsQuery | StickinessQuery - setQuery: (node: TrendsQuery | StickinessQuery) => void } -export function GlobalAndOrFilters({ query, setQuery }: GlobalAndOrFiltersProps): JSX.Element { +export function GlobalAndOrFilters({ query }: GlobalAndOrFiltersProps): JSX.Element { const { actions: allActions } = useValues(actionsModel) const { groupsTaxonomicTypes } = useValues(groupsModel) + const { updateQuerySource } = useActions(insightVizDataLogic) const taxonomicGroupTypes = [ TaxonomicFilterGroupType.EventProperties, @@ -31,7 +32,7 @@ export function GlobalAndOrFilters({ query, setQuery }: GlobalAndOrFiltersProps) { @@ -34,7 +35,16 @@ let uniqueNode = 0 export function InsightViz({ uniqueKey, query, setQuery, context, readOnly }: InsightVizProps): JSX.Element { const [key] = useState(() => `InsightViz.${uniqueKey || uniqueNode++}`) - const insightProps: InsightLogicProps = context?.insightProps || { dashboardItemId: `new-AdHoc.${key}`, query } + const insightProps: InsightLogicProps = context?.insightProps || { + dashboardItemId: `new-AdHoc.${key}`, + query, + setQuery, + } + + if (!insightProps.setQuery && setQuery) { + insightProps.setQuery = setQuery + } + const dataNodeLogicProps: DataNodeLogicProps = { query: query.source, key: insightVizDataNodeKey(insightProps), @@ -46,10 +56,6 @@ export function InsightViz({ uniqueKey, query, setQuery, context, readOnly }: In const isFunnels = isFunnelsQuery(query.source) - const setQuerySource = (source: InsightQueryNode): void => { - setQuery?.({ ...query, source }) - } - const showIfFull = !!query.full const disableHeader = !(query.showHeader ?? showIfFull) const disableTable = !(query.showTable ?? showIfFull) @@ -63,35 +69,32 @@ export function InsightViz({ uniqueKey, query, setQuery, context, readOnly }: In return ( -
- {!readOnly && ( - - )} + +
+ {!readOnly && ( + + )} - {showingResults && ( -
- -
- )} -
+ {showingResults && ( +
+ +
+ )} +
+
) diff --git a/frontend/src/queries/nodes/InsightViz/LifecycleToggles.tsx b/frontend/src/queries/nodes/InsightViz/LifecycleToggles.tsx index 0c604833e7170..d40ffa1170c40 100644 --- a/frontend/src/queries/nodes/InsightViz/LifecycleToggles.tsx +++ b/frontend/src/queries/nodes/InsightViz/LifecycleToggles.tsx @@ -1,6 +1,8 @@ import { LifecycleQuery } from '~/queries/schema' import { LifecycleToggle } from '~/types' import { LemonCheckbox, LemonLabel } from '@posthog/lemon-ui' +import { useActions } from 'kea' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' const lifecycles: { name: LifecycleToggle; tooltip: string; color: string }[] = [ { @@ -29,21 +31,22 @@ const lifecycles: { name: LifecycleToggle; tooltip: string; color: string }[] = type LifecycleTogglesProps = { query: LifecycleQuery - setQuery: (node: LifecycleQuery) => void } const DEFAULT_LIFECYCLE_TOGGLES: LifecycleToggle[] = ['new', 'returning', 'resurrecting', 'dormant'] -export function LifecycleToggles({ query, setQuery }: LifecycleTogglesProps): JSX.Element { +export function LifecycleToggles({ query }: LifecycleTogglesProps): JSX.Element { const toggledLifecycles = query.lifecycleFilter?.toggledLifecycles || DEFAULT_LIFECYCLE_TOGGLES + const { updateQuerySource } = useActions(insightVizDataLogic) + const setToggledLifecycles = (lifecycles: LifecycleToggle[]): void => { - setQuery({ + updateQuerySource({ ...query, lifecycleFilter: { ...query.lifecycleFilter, toggledLifecycles: lifecycles, }, - }) + } as LifecycleQuery) } const toggleLifecycle = (name: LifecycleToggle): void => { diff --git a/frontend/src/queries/nodes/InsightViz/TrendsSeries.tsx b/frontend/src/queries/nodes/InsightViz/TrendsSeries.tsx index bc62eb7f36bf2..501608e7abcbd 100644 --- a/frontend/src/queries/nodes/InsightViz/TrendsSeries.tsx +++ b/frontend/src/queries/nodes/InsightViz/TrendsSeries.tsx @@ -1,7 +1,7 @@ import { useValues, useActions } from 'kea' import { groupsModel } from '~/models/groupsModel' import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' -import { InsightType, FilterType, InsightLogicProps } from '~/types' +import { InsightType, FilterType } from '~/types' import { alphabet } from 'lib/utils' import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' @@ -13,15 +13,10 @@ import { actionsAndEventsToSeries } from '../InsightQuery/utils/filtersToQueryNo import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -type TrendsSeriesProps = { - insightProps: InsightLogicProps -} +export function TrendsSeries(): JSX.Element | null { + const { querySource, isTrends, isLifecycle, isStickiness, display, hasFormula } = useValues(insightVizDataLogic) + const { updateQuerySource } = useActions(insightVizDataLogic) -export function TrendsSeries({ insightProps }: TrendsSeriesProps): JSX.Element | null { - const { querySource, isTrends, isLifecycle, isStickiness, display, hasFormula } = useValues( - insightVizDataLogic(insightProps) - ) - const { updateQuerySource } = useActions(insightVizDataLogic(insightProps)) const { groupsTaxonomicTypes } = useValues(groupsModel) const propertiesTaxonomicGroupTypes = [ diff --git a/frontend/src/queries/nodes/SavedInsight/SavedInsight.tsx b/frontend/src/queries/nodes/SavedInsight/SavedInsight.tsx index 947541fdb4e27..4ccc6660ca9b0 100644 --- a/frontend/src/queries/nodes/SavedInsight/SavedInsight.tsx +++ b/frontend/src/queries/nodes/SavedInsight/SavedInsight.tsx @@ -2,11 +2,11 @@ import { useValues } from 'kea' import { insightLogic } from 'scenes/insights/insightLogic' import { Query } from '~/queries/Query/Query' -import { SavedInsightNode, NodeKind, QueryContext } from '~/queries/schema' +import { SavedInsightNode, QueryContext } from '~/queries/schema' import { InsightLogicProps, InsightModel } from '~/types' import { Animation } from 'lib/components/Animation/Animation' import { AnimationType } from 'lib/animations/animations' -import { filtersToQueryNode } from '../InsightQuery/utils/filtersToQueryNode' +import { insightDataLogic } from 'scenes/insights/insightDataLogic' interface InsightProps { query: SavedInsightNode @@ -14,9 +14,10 @@ interface InsightProps { context?: QueryContext } -export function SavedInsight({ query, context, cachedResults }: InsightProps): JSX.Element { - const insightProps: InsightLogicProps = { dashboardItemId: query.shortId, cachedInsight: cachedResults } +export function SavedInsight({ query: propsQuery, context, cachedResults }: InsightProps): JSX.Element { + const insightProps: InsightLogicProps = { dashboardItemId: propsQuery.shortId, cachedInsight: cachedResults } const { insight, insightLoading } = useValues(insightLogic(insightProps)) + const { query: dataQuery } = useValues(insightDataLogic(insightProps)) if (insightLoading) { return ( @@ -30,10 +31,7 @@ export function SavedInsight({ query, context, cachedResults }: InsightProps): J throw new Error('InsightNode expects an insight with filters') } - return ( - - ) + const query = { ...propsQuery, ...dataQuery, full: propsQuery.full } + + return } diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 06ff315deeb24..2621f27fa3a64 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -10,6 +10,7 @@ import { isTimeToSeeDataSessionsNode, isHogQLQuery, isInsightVizNode, + isLifecycleQuery, } from './utils' import api, { ApiMethodOptions } from 'lib/api' import { getCurrentTeamId } from 'lib/utils/logics' @@ -27,6 +28,8 @@ import { toParams } from 'lib/utils' import { queryNodeToFilter } from './nodes/InsightQuery/utils/queryNodeToFilter' import { now } from 'lib/dayjs' import { currentSessionId } from 'lib/internalMetrics' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' const EXPORT_MAX_LIMIT = 10000 @@ -104,10 +107,14 @@ export async function query( const logParams: Record = {} const startTime = performance.now() + const hogQLInsightsFlagEnabled = Boolean( + featureFlagLogic.findMounted()?.values.featureFlags?.[FEATURE_FLAGS.HOGQL_INSIGHTS] + ) + try { if (isPersonsNode(queryNode)) { response = await api.get(getPersonsEndpoint(queryNode), methodOptions) - } else if (isInsightQueryNode(queryNode)) { + } else if (isInsightQueryNode(queryNode) && !(hogQLInsightsFlagEnabled && isLifecycleQuery(queryNode))) { const filters = queryNodeToFilter(queryNode) const params = { ...filters, diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 4412d012c5efb..792812f2bd585 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1411,6 +1411,9 @@ ], "description": "Property filters for all series" }, + "response": { + "$ref": "#/definitions/LifecycleQueryResponse" + }, "samplingFactor": { "description": "Sampling rate", "type": ["number", "null"] @@ -1433,6 +1436,25 @@ "required": ["kind", "series"], "type": "object" }, + "LifecycleQueryResponse": { + "additionalProperties": false, + "properties": { + "result": { + "items": { + "type": "object" + }, + "type": "array" + }, + "timings": { + "items": { + "$ref": "#/definitions/QueryTiming" + }, + "type": "array" + } + }, + "required": ["result"], + "type": "object" + }, "LifecycleToggle": { "enum": ["new", "resurrecting", "returning", "dormant"], "type": "string" @@ -1888,10 +1910,18 @@ "SavedInsightNode": { "additionalProperties": false, "properties": { + "allowSorting": { + "description": "Can the user click on column headers to sort the table? (default: true)", + "type": "boolean" + }, "embedded": { "description": "Query is embedded inside another bordered component", "type": "boolean" }, + "expandable": { + "description": "Can expand row to show raw event data (default: true)", + "type": "boolean" + }, "full": { "description": "Show with most visual options enabled. Used in insight scene.", "type": "boolean" @@ -1900,29 +1930,93 @@ "const": "SavedInsightNode", "type": "string" }, + "propertiesViaUrl": { + "description": "Link properties via the URL (default: false)", + "type": "boolean" + }, "shortId": { "$ref": "#/definitions/InsightShortId" }, + "showActions": { + "description": "Show the kebab menu at the end of the row", + "type": "boolean" + }, + "showColumnConfigurator": { + "description": "Show a button to configure the table's columns if possible", + "type": "boolean" + }, "showCorrelationTable": { "type": "boolean" }, + "showDateRange": { + "description": "Show date range selector", + "type": "boolean" + }, + "showElapsedTime": { + "description": "Show the time it takes to run a query", + "type": "boolean" + }, + "showEventFilter": { + "description": "Include an event filter above the table (EventsNode only)", + "type": "boolean" + }, + "showExport": { + "description": "Show the export button", + "type": "boolean" + }, "showFilters": { "type": "boolean" }, "showHeader": { "type": "boolean" }, + "showHogQLEditor": { + "description": "Include a HogQL query editor above HogQL tables", + "type": "boolean" + }, "showLastComputation": { "type": "boolean" }, "showLastComputationRefresh": { "type": "boolean" }, + "showOpenEditorButton": { + "description": "Show a button to open the current query as a new insight. (default: true)", + "type": "boolean" + }, + "showPersistentColumnConfigurator": { + "description": "Show a button to configure and persist the table's default columns if possible", + "type": "boolean" + }, + "showPropertyFilter": { + "description": "Include a property filter above the table", + "type": "boolean" + }, + "showReload": { + "description": "Show a reload button", + "type": "boolean" + }, "showResults": { "type": "boolean" }, + "showResultsTable": { + "description": "Show a results table", + "type": "boolean" + }, + "showSavedQueries": { + "description": "Shows a list of saved queries", + "type": "boolean" + }, + "showSearch": { + "description": "Include a free text search field (PersonsNode only)", + "type": "boolean" + }, "showTable": { "type": "boolean" + }, + "showTimings": { + "description": "Show a detailed query timing breakdown", + "type": "boolean" } }, "required": ["kind", "shortId"], diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 1a7814cc71cb7..1d5cd9e689d31 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -273,7 +273,7 @@ export interface PersonsNode extends DataNode { export type HasPropertiesNode = EventsNode | EventsQuery | PersonsNode -export interface DataTableNode extends Node { +export interface DataTableNode extends Node, DataTableNodeViewProps { kind: NodeKind.DataTableNode /** Source of the events */ source: EventsNode | EventsQuery | PersonsNode | HogQLQuery | TimeToSeeDataSessionsQuery @@ -282,8 +282,10 @@ export interface DataTableNode extends Node { columns?: HogQLExpression[] /** Columns that aren't shown in the table, even if in columns or returned data */ hiddenColumns?: HogQLExpression[] - /** Show with most visual options enabled. Used in scenes. */ - full?: boolean +} + +interface DataTableNodeViewProps { + /** Show with most visual options enabled. Used in scenes. */ full?: boolean /** Include an event filter above the table (EventsNode only) */ showEventFilter?: boolean /** Include a free text search field (PersonsNode only) */ @@ -326,7 +328,7 @@ export interface DataTableNode extends Node { // Saved insight node -export interface SavedInsightNode extends Node, InsightVizNodeViewProps { +export interface SavedInsightNode extends Node, InsightVizNodeViewProps, DataTableNodeViewProps { kind: NodeKind.SavedInsightNode shortId: InsightShortId } @@ -440,6 +442,11 @@ export type LifecycleFilter = Omit & { toggledLifecycles?: LifecycleToggle[] } // using everything except what it inherits from FilterType +export interface LifecycleQueryResponse { + result: Record[] + timings?: QueryTiming[] +} + export interface LifecycleQuery extends InsightsQueryBase { kind: NodeKind.LifecycleQuery /** Granularity of the response. Can be one of `hour`, `day`, `week` or `month` */ @@ -448,6 +455,7 @@ export interface LifecycleQuery extends InsightsQueryBase { series: (EventsNode | ActionsNode)[] /** Properties specific to the lifecycle insight */ lifecycleFilter?: LifecycleFilter + response?: LifecycleQueryResponse } export type InsightQueryNode = diff --git a/frontend/src/scenes/actions/ActionEdit.tsx b/frontend/src/scenes/actions/ActionEdit.tsx index 86ef1aaa5bd82..6126e9eacce85 100644 --- a/frontend/src/scenes/actions/ActionEdit.tsx +++ b/frontend/src/scenes/actions/ActionEdit.tsx @@ -101,6 +101,7 @@ export function ActionEdit({ action: loadedAction, id, onSave, temporaryToken }: any> = { [Scene.EarlyAccessFeature]: () => import('./early-access-features/EarlyAccessFeature'), [Scene.Surveys]: () => import('./surveys/Surveys'), [Scene.Survey]: () => import('./surveys/Survey'), - [Scene.DataWarehouse]: () => import('./data-warehouse/posthog/DataWarehousePosthogScene'), + [Scene.DataWarehouse]: () => import('./data-warehouse/external/DataWarehouseExternalScene'), [Scene.DataWarehousePosthog]: () => import('./data-warehouse/posthog/DataWarehousePosthogScene'), [Scene.DataWarehouseExternal]: () => import('./data-warehouse/external/DataWarehouseExternalScene'), [Scene.DataWarehouseSavedQueries]: () => import('./data-warehouse/saved_queries/DataWarehouseSavedQueriesScene'), diff --git a/frontend/src/scenes/authentication/Login.tsx b/frontend/src/scenes/authentication/Login.tsx index 883c3215db9bb..b3c3cf1f70149 100644 --- a/frontend/src/scenes/authentication/Login.tsx +++ b/frontend/src/scenes/authentication/Login.tsx @@ -168,7 +168,9 @@ export function Login(): JSX.Element {
)} - + {!precheckResponse.saml_available && !precheckResponse.sso_enforcement && ( + + )}
) diff --git a/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel2.tsx b/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel2.tsx index 4922c74d836ab..07ea86b6bd31a 100644 --- a/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel2.tsx +++ b/frontend/src/scenes/authentication/signup/signupForm/panels/SignupPanel2.tsx @@ -5,16 +5,13 @@ import SignupRoleSelect from 'lib/components/SignupRoleSelect' import { Field } from 'lib/forms/Field' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { signupLogic } from '../signupLogic' -import SignupReferralSourceSelect from 'lib/components/SignupReferralSourceSelect' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { FEATURE_FLAGS } from 'lib/constants' +import SignupReferralSource from 'lib/components/SignupReferralSource' const UTM_TAGS = 'utm_campaign=in-product&utm_tag=signup-header' export function SignupPanel2(): JSX.Element | null { const { preflight } = useValues(preflightLogic) const { isSignupPanel2Submitting } = useValues(signupLogic) - const { featureFlags } = useValues(featureFlagLogic) return (
@@ -36,20 +33,7 @@ export function SignupPanel2(): JSX.Element | null { /> - {featureFlags[FEATURE_FLAGS.REFERRAL_SOURCE_SELECT] === 'test' ? ( - - ) : ( - <> - - - - - )} +
+
+ + + + +
+ + + +
- - - +
+ + + {batchExportConfigForm.encryption == 'aws:kms' && ( + + + + )}
+ diff --git a/frontend/src/scenes/data-management/definition/DefinitionView.tsx b/frontend/src/scenes/data-management/definition/DefinitionView.tsx index bc07eba43fe68..bf3ef6a9abb07 100644 --- a/frontend/src/scenes/data-management/definition/DefinitionView.tsx +++ b/frontend/src/scenes/data-management/definition/DefinitionView.tsx @@ -83,6 +83,7 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { ({ }, reducers: { tab: [ - DataWarehouseTab.Posthog as DataWarehouseTab, + DataWarehouseTab.External as DataWarehouseTab, { setTab: (_, { tab }) => tab, }, @@ -58,14 +58,14 @@ export function DataWarehousePageTabs({ tab }: { tab: DataWarehouseTab }): JSX.E activeKey={tab} onChange={(t) => setTab(t)} tabs={[ - { - key: DataWarehouseTab.Posthog, - label: Posthog, - }, { key: DataWarehouseTab.External, label: External, }, + { + key: DataWarehouseTab.Posthog, + label: Posthog, + }, ...(featureFlags[FEATURE_FLAGS.DATA_WAREHOUSE_VIEWS] ? [ { diff --git a/frontend/src/scenes/data-warehouse/external/DataWarehouseExternalScene.tsx b/frontend/src/scenes/data-warehouse/external/DataWarehouseExternalScene.tsx index a4750db49090c..4b163af4135f4 100644 --- a/frontend/src/scenes/data-warehouse/external/DataWarehouseExternalScene.tsx +++ b/frontend/src/scenes/data-warehouse/external/DataWarehouseExternalScene.tsx @@ -30,13 +30,15 @@ export function DataWarehouseExternalScene(): JSX.Element {
} buttons={ - - New Table - + !shouldShowProductIntroduction ? ( + + New Table + + ) : undefined } caption={
diff --git a/frontend/src/scenes/experiments/Experiment.tsx b/frontend/src/scenes/experiments/Experiment.tsx index b8b5b7a9eec06..0256b46967d39 100644 --- a/frontend/src/scenes/experiments/Experiment.tsx +++ b/frontend/src/scenes/experiments/Experiment.tsx @@ -615,6 +615,7 @@ export function Experiment(): JSX.Element { {isExperimentRunning ? ( Test that it works - {`posthog.feature_flags.override({'${flagKey}': '${variant}'})`} + {`posthog.featureFlags.override({'${flagKey}': '${variant}'})`} ) diff --git a/frontend/src/scenes/experiments/MetricSelector.tsx b/frontend/src/scenes/experiments/MetricSelector.tsx index 307bbd61c7762..b21286f758f92 100644 --- a/frontend/src/scenes/experiments/MetricSelector.tsx +++ b/frontend/src/scenes/experiments/MetricSelector.tsx @@ -133,11 +133,7 @@ export function ExperimentInsightCreator({ insightProps }: { insightProps: Insig
- + )} @@ -146,7 +142,7 @@ export function ExperimentInsightCreator({ insightProps }: { insightProps: Insig ) } -export function AttributionSelect({ insightProps, query, setQuery }: EditorFilterProps): JSX.Element { +export function AttributionSelect({ insightProps, query }: EditorFilterProps): JSX.Element { return (
@@ -170,7 +166,7 @@ export function AttributionSelect({ insightProps, query, setQuery }: EditorFilte - +
) } diff --git a/frontend/src/scenes/feature-flags/FeatureFlagCodeInstructions.stories.tsx b/frontend/src/scenes/feature-flags/FeatureFlagCodeInstructions.stories.tsx index 642ef3e6a88a6..5ac711aabde81 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagCodeInstructions.stories.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagCodeInstructions.stories.tsx @@ -29,6 +29,7 @@ const REGULAR_FEATURE_FLAG: FeatureFlagType = { performed_rollback: false, can_edit: true, tags: [], + surveys: [], } const GROUP_FEATURE_FLAG: FeatureFlagType = { diff --git a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx index 140a5c3cc47b7..e320157674c5b 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx @@ -394,7 +394,7 @@ export function FeatureFlagReleaseConditions({ return ( <>
-
+
{readOnly ? ( excludeTitle ? null : (

{isSuper ? 'Super Release Conditions' : 'Release conditions'}

diff --git a/frontend/src/scenes/feature-flags/activityDescriptions.tsx b/frontend/src/scenes/feature-flags/activityDescriptions.tsx index 1210c89089463..f774616afe7ba 100644 --- a/frontend/src/scenes/feature-flags/activityDescriptions.tsx +++ b/frontend/src/scenes/feature-flags/activityDescriptions.tsx @@ -250,6 +250,7 @@ const featureFlagActionsMapping: Record< can_edit: () => null, analytics_dashboards: () => null, has_enriched_analytics: () => null, + surveys: () => null, } export function flagActivityDescriber(logItem: ActivityLogItem, asNotification?: boolean): HumanizedChange { diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.test.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.test.ts index 23aedb1086bba..f0516fe9956e1 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.test.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.test.ts @@ -37,6 +37,7 @@ function generateFeatureFlag( usage_dashboard: 1234, tags: [], has_enriched_analytics, + surveys: [], } } diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.ts index 24f90439d16f8..aeb4b9471f764 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.ts @@ -19,6 +19,8 @@ import { DashboardBasicType, NewEarlyAccessFeatureType, EarlyAccessFeatureType, + Survey, + SurveyQuestionType, } from '~/types' import api from 'lib/api' import { router, urlToAction } from 'kea-router' @@ -40,6 +42,7 @@ import { userLogic } from 'scenes/userLogic' import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' import { dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic' import { NEW_EARLY_ACCESS_FEATURE } from 'scenes/early-access-features/earlyAccessFeatureLogic' +import { NEW_SURVEY, NewSurvey } from 'scenes/surveys/surveyLogic' const getDefaultRollbackCondition = (): FeatureFlagRollbackConditions => ({ operator: 'gt', @@ -73,6 +76,7 @@ const NEW_FLAG: FeatureFlagType = { experiment_set: null, features: [], rollback_conditions: [], + surveys: null, performed_rollback: false, can_edit: true, tags: [], @@ -414,6 +418,15 @@ export const featureFlagLogic = kea([ features: [...(state.features || []), newEarlyAccessFeature], } }, + createSurveySuccess: (state, { newSurvey }) => { + if (!state) { + return state + } + return { + ...state, + surveys: [...(state.surveys || []), newSurvey], + } + }, }, ], featureFlagMissing: [false, { setFeatureFlagMissing: () => true }], @@ -520,12 +533,33 @@ export const featureFlagLogic = kea([ null as EarlyAccessFeatureType | null, { createEarlyAccessFeature: async () => { - const updatedEarlyAccessFeature = { + const newEarlyAccessFeature = { ...NEW_EARLY_ACCESS_FEATURE, name: `Early access: ${values.featureFlag.key}`, feature_flag_id: values.featureFlag.id, } - return await api.earlyAccessFeatures.create(updatedEarlyAccessFeature as NewEarlyAccessFeatureType) + return await api.earlyAccessFeatures.create(newEarlyAccessFeature as NewEarlyAccessFeatureType) + }, + }, + ], + // used to generate a new survey + // but all subsequent operations after generation should occur via the surveyLogic + newSurvey: [ + null as Survey | null, + { + createSurvey: async () => { + const newSurvey = { + ...NEW_SURVEY, + name: `Survey: ${values.featureFlag.key}`, + linked_flag_id: values.featureFlag.id, + questions: [ + { + type: SurveyQuestionType.Open, + question: `What do you think of ${values.featureFlag.key}?`, + }, + ], + } + return await api.surveys.create(newSurvey as NewSurvey) }, }, ], @@ -869,6 +903,22 @@ export const featureFlagLogic = kea([ return (featureFlag?.features?.length || 0) > 0 }, ], + canCreateEarlyAccessFeature: [ + (s) => [s.featureFlag, s.variants], + (featureFlag, variants) => { + return ( + featureFlag && + featureFlag.filters.aggregation_group_type_index == undefined && + variants.length === 0 + ) + }, + ], + hasSurveys: [ + (s) => [s.featureFlag], + (featureFlag) => { + return featureFlag?.surveys && featureFlag.surveys.length > 0 + }, + ], }), urlToAction(({ actions, props }) => ({ [urls.featureFlag(props.id ?? 'new')]: (_, __, ___, { method }) => { diff --git a/frontend/src/scenes/insights/Insight.tsx b/frontend/src/scenes/insights/Insight.tsx index faea92fc4ec7c..e782500ebf652 100644 --- a/frontend/src/scenes/insights/Insight.tsx +++ b/frontend/src/scenes/insights/Insight.tsx @@ -30,7 +30,6 @@ export function Insight({ insightId }: InsightSceneProps): JSX.Element { // insightDataLogic const { query, isQueryBasedInsight, showQueryEditor } = useValues(insightDataLogic(insightProps)) - const { setQuery } = useActions(insightDataLogic(insightProps)) // other logics useMountedLogic(insightCommandLogic(insightProps)) @@ -58,7 +57,6 @@ export function Insight({ insightId }: InsightSceneProps): JSX.Element { ([ timezone: [(s) => [s.insightData], (insightData) => insightData?.timezone || 'UTC'], }), - listeners(({ actions, values }) => ({ + listeners(({ actions, values, props }) => ({ updateDateRange: ({ dateRange }) => { const localQuerySource = values.querySource ? values.querySource @@ -242,6 +242,10 @@ export const insightVizDataLogic = kea([ } }, setQuery: ({ query }) => { + if (props.setQuery) { + props.setQuery(query as InsightVizNode) + } + if (isInsightVizNode(query)) { const querySource = query.source const filters = queryNodeToFilter(querySource) diff --git a/frontend/src/scenes/instance/SystemStatus/index.tsx b/frontend/src/scenes/instance/SystemStatus/index.tsx index 95351c18e224e..11adb42107c21 100644 --- a/frontend/src/scenes/instance/SystemStatus/index.tsx +++ b/frontend/src/scenes/instance/SystemStatus/index.tsx @@ -30,7 +30,7 @@ export function SystemStatus(): JSX.Element { const { user } = useValues(userLogic) const { featureFlags } = useValues(featureFlagLogic) - const tabs = [ + let tabs = [ { key: 'overview', label: 'System overview', @@ -39,7 +39,7 @@ export function SystemStatus(): JSX.Element { ] as LemonTab[] if (user?.is_staff) { - tabs.concat([ + tabs = tabs.concat([ { key: 'metrics', label: 'Internal metrics', diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx index 5f58cad124a1b..6d90866fe208e 100644 --- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx @@ -6,6 +6,7 @@ import { ExtendedRegExpMatchArray, Attribute, NodeViewProps, + getExtensionField, } from '@tiptap/react' import { ReactNode, useCallback, useRef } from 'react' import clsx from 'clsx' @@ -31,7 +32,7 @@ export interface NodeWrapperProps { title: string | ((attributes: CustomNotebookNodeAttributes) => Promise) nodeType: NotebookNodeType children?: ReactNode | ((isEdit: boolean, isPreview: boolean) => ReactNode) - href?: string | ((attributes: NotebookNodeAttributes) => string) + href?: string | ((attributes: NotebookNodeAttributes) => string | undefined) // Sizing expandable?: boolean @@ -66,7 +67,8 @@ export function NodeWrapper({ widgets = [], }: NodeWrapperProps & NotebookNodeViewProps): JSX.Element { const mountedNotebookLogic = useMountedLogic(notebookLogic) - const { isEditable } = useValues(mountedNotebookLogic) + const { isEditable, isShowingSidebar } = useValues(mountedNotebookLogic) + const { setIsShowingSidebar } = useActions(mountedNotebookLogic) // nodeId can start null, but should then immediately be generated const nodeId = attributes.nodeId @@ -85,7 +87,7 @@ export function NodeWrapper({ } const nodeLogic = useMountedLogic(notebookNodeLogic(nodeLogicProps)) const { title, resizeable, expanded } = useValues(nodeLogic) - const { setExpanded, deleteNode, setWidgetsVisible } = useActions(nodeLogic) + const { setExpanded, deleteNode } = useActions(nodeLogic) const [ref, inView] = useInView({ triggerOnce: true }) const contentRef = useRef(null) @@ -154,32 +156,35 @@ export function NodeWrapper({ {title} - {parsedHref && } to={parsedHref} />} +
+ {parsedHref && } to={parsedHref} />} - {expandable && ( - setExpanded(!expanded)} - size="small" - icon={expanded ? : } - /> - )} + {expandable && ( + setExpanded(!expanded)} + size="small" + icon={expanded ? : } + /> + )} - {!!widgets.length && isEditable ? ( - setWidgetsVisible(true)} - size="small" - icon={} - /> - ) : null} + {widgets.length > 0 ? ( + setIsShowingSidebar(!isShowingSidebar)} + size="small" + icon={} + active={isShowingSidebar && selected} + /> + ) : null} - {isEditable && ( - deleteNode()} - size="small" - status="danger" - icon={} - /> - )} + {isEditable && ( + deleteNode()} + size="small" + status="danger" + icon={} + /> + )} +
> widgets?: NotebookNodeWidget[] + serializedText?: (attributes: NotebookNodeAttributes) => string } export function createPostHogWidgetNode({ Component, pasteOptions, attributes, + serializedText, ...wrapperProps }: CreatePostHogWidgetNodeOptions): Node { // NOTE: We use NodeViewProps here as we convert them to NotebookNodeViewProps @@ -252,6 +259,19 @@ export function createPostHogWidgetNode( atom: true, draggable: true, + serializedText: serializedText, + + extendNodeSchema(extension) { + const context = { + name: extension.name, + options: extension.options, + storage: extension.storage, + } + return { + serializedText: getExtensionField(extension, 'serializedText', context), + } + }, + addAttributes() { return { height: {}, diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx index 9935f9c6f1608..154600a7e1d3f 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx @@ -2,7 +2,16 @@ import { mergeAttributes, Node, NodeViewProps } from '@tiptap/core' import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react' import { InsightModel, NotebookNodeType, NotebookTarget } from '~/types' import { Link } from '@posthog/lemon-ui' -import { IconGauge, IconBarChart, IconFlag, IconExperiment, IconLive, IconPerson, IconCohort } from 'lib/lemon-ui/icons' +import { + IconGauge, + IconBarChart, + IconFlag, + IconExperiment, + IconLive, + IconPerson, + IconCohort, + IconJournal, +} from 'lib/lemon-ui/icons' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { urls } from 'scenes/urls' import clsx from 'clsx' @@ -22,6 +31,7 @@ const ICON_MAP = { events: , persons: , cohorts: , + notebooks: , } const Component = (props: NodeViewProps): JSX.Element => { @@ -67,6 +77,8 @@ function backlinkHref(id: string, type: TaxonomicFilterGroupType): string { return urls.experiment(id) } else if (type === TaxonomicFilterGroupType.Dashboards) { return urls.dashboard(id) + } else if (type === TaxonomicFilterGroupType.Notebooks) { + return urls.notebook(id) } return '' } @@ -139,6 +151,16 @@ export const NotebookNodeBacklink = Node.create({ return { id: id, type: TaxonomicFilterGroupType.Dashboards, title: dashboard.name } }, }), + posthogNodePasteRule({ + find: urls.notebook('(.+)'), + editor: this.editor, + type: this.type, + getAttributes: async (match) => { + const id = match[1] + const notebook = await api.notebooks.get(id) + return { id: id, type: TaxonomicFilterGroupType.Notebooks, title: notebook.title } + }, + }), ] }, }) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx index 0e315def449b9..066917f6f3c9a 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx @@ -2,7 +2,7 @@ import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper' import { FeatureFlagType, NotebookNodeType } from '~/types' import { BindLogic, useActions, useValues } from 'kea' import { featureFlagLogic, FeatureFlagLogicProps } from 'scenes/feature-flags/featureFlagLogic' -import { IconFlag, IconRecording, IconRocketLaunch } from 'lib/lemon-ui/icons' +import { IconFlag, IconRecording, IconRocketLaunch, IconSurveys } from 'lib/lemon-ui/icons' import clsx from 'clsx' import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import { urls } from 'scenes/urls' @@ -15,6 +15,7 @@ import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagRe import api from 'lib/api' import { buildEarlyAccessFeatureContent } from './NotebookNodeEarlyAccessFeature' import { notebookNodeFlagLogic } from './NotebookNodeFlagLogic' +import { buildSurveyContent } from './NotebookNodeSurvey' const Component = (props: NotebookNodeViewProps): JSX.Element => { const { id } = props.attributes @@ -24,12 +25,17 @@ const Component = (props: NotebookNodeViewProps): JS recordingFilterForFlag, hasEarlyAccessFeatures, newEarlyAccessFeatureLoading, + canCreateEarlyAccessFeature, + hasSurveys, + newSurveyLoading, } = useValues(featureFlagLogic({ id })) - const { createEarlyAccessFeature } = useActions(featureFlagLogic({ id })) + const { createEarlyAccessFeature, createSurvey } = useActions(featureFlagLogic({ id })) const { expanded, nextNode } = useValues(notebookNodeLogic) const { insertAfter } = useActions(notebookNodeLogic) - const { shouldDisableInsertEarlyAccessFeature } = useValues(notebookNodeFlagLogic({ id, insertAfter })) + const { shouldDisableInsertEarlyAccessFeature, shouldDisableInsertSurvey } = useValues( + notebookNodeFlagLogic({ id, insertAfter }) + ) return (
@@ -64,37 +70,67 @@ const Component = (props: NotebookNodeViewProps): JS
+ {canCreateEarlyAccessFeature && ( + } + loading={newEarlyAccessFeatureLoading} + onClick={(e) => { + // prevent expanding the node if it isn't expanded + e.stopPropagation() + + if (!hasEarlyAccessFeatures) { + createEarlyAccessFeature() + } else { + if ((featureFlag?.features?.length || 0) <= 0) { + return + } + if (!shouldDisableInsertEarlyAccessFeature(nextNode) && featureFlag.features) { + insertAfter(buildEarlyAccessFeatureContent(featureFlag.features[0].id)) + } + } + }} + disabledReason={ + shouldDisableInsertEarlyAccessFeature(nextNode) && + 'Early access feature already exists below' + } + > + {hasEarlyAccessFeatures ? 'View' : 'Create'} early access feature + + )} } - loading={newEarlyAccessFeatureLoading} + icon={} + loading={newSurveyLoading} onClick={(e) => { // prevent expanding the node if it isn't expanded e.stopPropagation() - if (!hasEarlyAccessFeatures) { - createEarlyAccessFeature() + + if (!hasSurveys) { + createSurvey() } else { - if ((featureFlag?.features?.length || 0) <= 0) { + if ((featureFlag?.surveys?.length || 0) <= 0) { return } - if (!shouldDisableInsertEarlyAccessFeature(nextNode) && featureFlag.features) { - insertAfter(buildEarlyAccessFeatureContent(featureFlag.features[0].id)) + if (!shouldDisableInsertSurvey(nextNode) && featureFlag.surveys) { + insertAfter(buildSurveyContent(featureFlag.surveys[0].id)) } } }} - disabledReason={ - shouldDisableInsertEarlyAccessFeature(nextNode) && - 'Early access feature already exists below' - } + disabledReason={shouldDisableInsertSurvey(nextNode) && 'Survey already exists below'} > - {hasEarlyAccessFeatures ? 'View' : 'Create'} early access feature + {hasSurveys ? 'View' : 'Create'} survey } - onClick={() => { + onClick={(e) => { + // prevent expanding the node if it isn't expanded + e.stopPropagation() + if (nextNode?.type.name !== NotebookNodeType.FeatureFlagCodeExample) { insertAfter(buildCodeExampleContent(id)) } @@ -107,7 +143,10 @@ const Component = (props: NotebookNodeViewProps): JS Show implementation { + onClick={(e) => { + // prevent expanding the node if it isn't expanded + e.stopPropagation() + if (nextNode?.type.name !== NotebookNodeType.RecordingPlaylist) { insertAfter(buildPlaylistContent(recordingFilterForFlag)) } diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagLogic.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagLogic.tsx index b597575854e69..aa0ed54d437d7 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagLogic.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagLogic.tsx @@ -5,6 +5,7 @@ import { buildEarlyAccessFeatureContent } from './NotebookNodeEarlyAccessFeature import { NotebookNodeType } from '~/types' import type { notebookNodeFlagLogicType } from './NotebookNodeFlagLogicType' +import { buildSurveyContent } from './NotebookNodeSurvey' export type NotebookNodeFlagLogicProps = { id: FeatureFlagLogicProps['id'] @@ -17,13 +18,16 @@ export const notebookNodeFlagLogic = kea([ key(({ id }) => id), connect((props: NotebookNodeFlagLogicProps) => ({ - actions: [featureFlagLogic({ id: props.id }), ['createEarlyAccessFeatureSuccess']], - values: [featureFlagLogic({ id: props.id }), ['featureFlag', 'hasEarlyAccessFeatures']], + actions: [featureFlagLogic({ id: props.id }), ['createEarlyAccessFeatureSuccess', 'createSurveySuccess']], + values: [featureFlagLogic({ id: props.id }), ['featureFlag', 'hasEarlyAccessFeatures', 'hasSurveys']], })), listeners(({ props }) => ({ createEarlyAccessFeatureSuccess: async ({ newEarlyAccessFeature }) => { props.insertAfter(buildEarlyAccessFeatureContent(newEarlyAccessFeature.id)) }, + createSurveySuccess: async ({ newSurvey }) => { + props.insertAfter(buildSurveyContent(newSurvey.id)) + }, })), selectors({ shouldDisableInsertEarlyAccessFeature: [ @@ -39,5 +43,18 @@ export const notebookNodeFlagLogic = kea([ ) }, ], + shouldDisableInsertSurvey: [ + (s) => [s.featureFlag, s.hasSurveys], + (featureFlag, hasSurveys) => + (nextNode: Node | null): boolean => { + return ( + (nextNode?.type.name === NotebookNodeType.Survey && + hasSurveys && + featureFlag.surveys && + nextNode?.attrs.id === featureFlag.surveys[0].id) || + false + ) + }, + ], }), ]) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx index effdf63d7afcf..8dc4e00839409 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx @@ -79,6 +79,10 @@ export const NotebookNodeImage = createPostHogWidgetNode { + // TODO file is null when this runs... should it be? + return attrs?.file?.name || '' + }, heightEstimate: 400, minHeight: 100, resizeable: true, diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx index a8640e956759a..d582171f9690a 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx @@ -76,4 +76,9 @@ export const NotebookNodePerson = createPostHogWidgetNode { + const personTitle = attrs?.title || '' + const personId = attrs?.id || '' + return `${personTitle} ${personId}`.trim() + }, }) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx index 0b801328e378e..0b0e3b7ca4ee8 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx @@ -14,7 +14,7 @@ import { SessionRecordingPlayer } from 'scenes/session-recordings/player/Session import { useMemo, useState } from 'react' import { fromParamsGivenUrl } from 'lib/utils' import { LemonButton } from '@posthog/lemon-ui' -import { IconChevronLeft, IconSettings } from 'lib/lemon-ui/icons' +import { IconChevronLeft } from 'lib/lemon-ui/icons' import { urls } from 'scenes/urls' import { notebookNodeLogic } from './notebookNodeLogic' import { JSONContent, NotebookNodeViewProps, NotebookNodeAttributeProperties } from '../Notebook/utils' @@ -130,7 +130,6 @@ export const NotebookNodePlaylist = createPostHogWidgetNode, Component: Settings, }, ], diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx index 3616fe485725a..f692d37023970 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx @@ -1,17 +1,19 @@ import { Query } from '~/queries/Query/Query' import { DataTableNode, InsightVizNode, NodeKind, QuerySchema } from '~/queries/schema' import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper' -import { useValues } from 'kea' -import { InsightShortId, NotebookNodeType } from '~/types' +import { InsightLogicProps, InsightShortId, NotebookNodeType } from '~/types' +import { useMountedLogic, useValues } from 'kea' import { useMemo } from 'react' import { notebookNodeLogic } from './notebookNodeLogic' import { NotebookNodeViewProps, NotebookNodeAttributeProperties } from '../Notebook/utils' +import { containsHogQLQuery, isHogQLQuery, isNodeWithSource } from '~/queries/utils' +import { LemonButton } from '@posthog/lemon-ui' import clsx from 'clsx' -import { IconSettings } from 'lib/lemon-ui/icons' import { urls } from 'scenes/urls' import api from 'lib/api' import './NotebookNodeQuery.scss' +import { insightDataLogic } from 'scenes/insights/insightDataLogic' const DEFAULT_QUERY: QuerySchema = { kind: NodeKind.DataTableNode, @@ -26,18 +28,21 @@ const DEFAULT_QUERY: QuerySchema = { const Component = (props: NotebookNodeViewProps): JSX.Element | null => { const { query } = props.attributes - const { expanded } = useValues(notebookNodeLogic) + const nodeLogic = useMountedLogic(notebookNodeLogic) + const { expanded } = useValues(nodeLogic) const modifiedQuery = useMemo(() => { - const modifiedQuery = { ...query } + const modifiedQuery = { ...query, full: false } - if (NodeKind.DataTableNode === modifiedQuery.kind) { + if (NodeKind.DataTableNode === modifiedQuery.kind || NodeKind.SavedInsightNode === modifiedQuery.kind) { // We don't want to show the insights button for now modifiedQuery.showOpenEditorButton = false modifiedQuery.full = false modifiedQuery.showHogQLEditor = false modifiedQuery.embedded = true - } else if (NodeKind.InsightVizNode === modifiedQuery.kind || NodeKind.SavedInsightNode === modifiedQuery.kind) { + } + + if (NodeKind.InsightVizNode === modifiedQuery.kind || NodeKind.SavedInsightNode === modifiedQuery.kind) { modifiedQuery.showFilters = false modifiedQuery.showHeader = false modifiedQuery.showTable = false @@ -56,7 +61,7 @@ const Component = (props: NotebookNodeViewProps): J
- +
) } @@ -69,29 +74,76 @@ export const Settings = ({ attributes, updateAttributes, }: NotebookNodeAttributeProperties): JSX.Element => { + const { query } = attributes + const modifiedQuery = useMemo(() => { - const modifiedQuery = { ...attributes.query } + const modifiedQuery = { ...query, full: false } - if (NodeKind.DataTableNode === modifiedQuery.kind) { + if (NodeKind.DataTableNode === modifiedQuery.kind || NodeKind.SavedInsightNode === modifiedQuery.kind) { // We don't want to show the insights button for now modifiedQuery.showOpenEditorButton = false modifiedQuery.showHogQLEditor = true modifiedQuery.showResultsTable = false modifiedQuery.showReload = false modifiedQuery.showElapsedTime = false - } else if (NodeKind.InsightVizNode === modifiedQuery.kind || NodeKind.SavedInsightNode === modifiedQuery.kind) { + modifiedQuery.embedded = true + } + + if (NodeKind.InsightVizNode === modifiedQuery.kind || NodeKind.SavedInsightNode === modifiedQuery.kind) { modifiedQuery.showFilters = true modifiedQuery.showResults = false modifiedQuery.embedded = true } return modifiedQuery - }, [attributes.query]) + }, [query]) - return ( + const detachSavedInsight = (): void => { + if (attributes.query.kind === NodeKind.SavedInsightNode) { + const insightProps: InsightLogicProps = { dashboardItemId: attributes.query.shortId } + const dataLogic = insightDataLogic.findMounted(insightProps) + + if (dataLogic) { + updateAttributes({ query: dataLogic.values.query as QuerySchema }) + } + } + } + + return attributes.query.kind === NodeKind.SavedInsightNode ? ( +
+
Insight created outside of this notebook
+
+ Changes made to the original insight will be reflected in the notebook. Or you can detach from the + insight to make changes independently in the notebook. +
+ +
+ + Edit the insight + + + Detach from insight + +
+
+ ) : (
{ updateAttributes({ query: { @@ -100,8 +152,6 @@ export const Settings = ({ } as QuerySchema, }) }} - readOnly={false} - uniqueKey={attributes.nodeId} />
) @@ -123,6 +173,12 @@ export const NotebookNodeQuery = createPostHogWidgetNode + attrs.query.kind === NodeKind.SavedInsightNode ? urls.insightView(attrs.query.shortId) : undefined, widgets: [ { key: 'settings', label: 'Settings', - icon: , Component: Settings, }, ], @@ -155,4 +212,17 @@ export const NotebookNodeQuery = createPostHogWidgetNode { + let text = '' + const q = attrs.query + if (containsHogQLQuery(q)) { + if (isHogQLQuery(q)) { + text = q.query + } + if (isNodeWithSource(q)) { + text = isHogQLQuery(q.source) ? q.source.query : '' + } + } + return text + }, }) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx index dafa271b98725..5004ba492124a 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx @@ -15,7 +15,6 @@ import { } from 'scenes/session-recordings/playlist/SessionRecordingPreview' import { notebookNodeLogic } from './notebookNodeLogic' import { LemonSwitch } from '@posthog/lemon-ui' -import { IconSettings } from 'lib/lemon-ui/icons' import { JSONContent, NotebookNodeViewProps, NotebookNodeAttributeProperties } from '../Notebook/utils' const HEIGHT = 500 @@ -102,10 +101,12 @@ export const NotebookNodeRecording = createPostHogWidgetNode, Component: Settings, }, ], + serializedText: (attrs) => { + return attrs.id + }, }) export function sessionRecordingPlayerProps(id: SessionRecordingId): SessionRecordingPlayerProps { diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeReplayTimestamp.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeReplayTimestamp.tsx index ec49f4445d005..88db6f4395ffc 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeReplayTimestamp.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeReplayTimestamp.tsx @@ -75,6 +75,12 @@ export const NotebookNodeReplayTimestamp = Node.create({ group: 'inline', atom: true, + serializedText: (attrs: NotebookNodeReplayTimestampAttrs): string => { + // timestamp is not a block so `getText` does not add a separator. + // we need to add it manually + return `${attrs.playbackTime ? formatTimestamp(attrs.playbackTime) : '00:00'}:\n` + }, + addAttributes() { return { playbackTime: { default: null, keepOnSplit: false }, diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx index 6ae601eeeab8b..d0b0cf87742b5 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx @@ -6,7 +6,7 @@ import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import { urls } from 'scenes/urls' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { notebookNodeLogic } from './notebookNodeLogic' -import { NotebookNodeViewProps } from '../Notebook/utils' +import { JSONContent, NotebookNodeViewProps } from '../Notebook/utils' import { buildFlagContent } from './NotebookNodeFlag' import { defaultSurveyAppearance, surveyLogic } from 'scenes/surveys/surveyLogic' import { StatusTag } from 'scenes/surveys/Surveys' @@ -140,3 +140,10 @@ export const NotebookNodeSurvey = createPostHogWidgetNode([ timestamp, sessionRecordingId, }), - setWidgetsVisible: (visible: boolean) => ({ visible }), setPreviousNode: (node: Node | null) => ({ node }), setNextNode: (node: Node | null) => ({ node }), deleteNode: true, @@ -112,22 +111,12 @@ export const notebookNodeLogic = kea([ setNextNode: (_, { node }) => node, }, ], - widgetsVisible: [ - false, - { - setWidgetsVisible: (_, { visible }) => visible, - }, - ], })), selectors({ notebookLogic: [(_, p) => [p.notebookLogic], (notebookLogic) => notebookLogic], nodeAttributes: [(_, p) => [p.attributes], (nodeAttributes) => nodeAttributes], widgets: [(_, p) => [p.widgets], (widgets) => widgets], - isShowingWidgets: [ - (s, p) => [s.widgetsVisible, p.widgets], - (widgetsVisible, widgets) => !!widgets.length && widgetsVisible, - ], }), listeners(({ actions, values, props }) => ({ diff --git a/frontend/src/scenes/notebooks/Notebook/BacklinkCommands.tsx b/frontend/src/scenes/notebooks/Notebook/BacklinkCommands.tsx index 49badffaf69e5..ef925ef805870 100644 --- a/frontend/src/scenes/notebooks/Notebook/BacklinkCommands.tsx +++ b/frontend/src/scenes/notebooks/Notebook/BacklinkCommands.tsx @@ -6,6 +6,7 @@ import { PluginKey } from '@tiptap/pm/state' import { Popover } from 'lib/lemon-ui/Popover' import { forwardRef } from 'react' import { + TaxonomicDefinitionTypes, TaxonomicFilterGroup, TaxonomicFilterGroupType, TaxonomicFilterLogicProps, @@ -41,18 +42,18 @@ const BacklinkCommands = forwardRef(functi const { editor } = useValues(notebookLogic) const onSelect = ( - { type }: TaxonomicFilterGroup, + group: TaxonomicFilterGroup, value: TaxonomicFilterValue, - { id, name }: { id: number; name: string } + item: TaxonomicDefinitionTypes ): void => { if (!editor) { return } const attrs = { - id: type === TaxonomicFilterGroupType.Events ? id : value, - title: name, - type: type, + id: group.type === TaxonomicFilterGroupType.Events ? item.id : value, + title: group.getName?.(item), + type: group.type, } editor @@ -81,6 +82,7 @@ const BacklinkCommands = forwardRef(functi TaxonomicFilterGroupType.FeatureFlags, TaxonomicFilterGroupType.Experiments, TaxonomicFilterGroupType.Dashboards, + TaxonomicFilterGroupType.Notebooks, ], optionsFromProp: undefined, popoverEnabled: true, diff --git a/frontend/src/scenes/notebooks/Notebook/Editor.tsx b/frontend/src/scenes/notebooks/Notebook/Editor.tsx index 60d1a67d08bcb..2a41bcce88209 100644 --- a/frontend/src/scenes/notebooks/Notebook/Editor.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Editor.tsx @@ -3,7 +3,7 @@ import { useActions } from 'kea' import { useCallback, useRef } from 'react' import { Editor as TTEditor } from '@tiptap/core' -import { useEditor, EditorContent } from '@tiptap/react' +import { EditorContent, useEditor } from '@tiptap/react' import { FloatingMenu } from '@tiptap/extension-floating-menu' import StarterKit from '@tiptap/starter-kit' import ExtensionPlaceholder from '@tiptap/extension-placeholder' @@ -25,7 +25,7 @@ import { lemonToast } from '@posthog/lemon-ui' import { NotebookNodeType } from '~/types' import { NotebookNodeImage } from '../Nodes/NotebookNodeImage' -import { JSONContent, NotebookEditor, EditorFocusPosition, EditorRange, Node } from './utils' +import { EditorFocusPosition, EditorRange, JSONContent, Node, NotebookEditor, textContent } from './utils' import { SlashCommandsExtension } from './SlashCommands' import { BacklinkCommandsExtension } from './BacklinkCommands' import { NotebookNodeEarlyAccessFeature } from '../Nodes/NotebookNodeEarlyAccessFeature' @@ -182,6 +182,7 @@ export function Editor({ onCreate({ getJSON: () => editor.getJSON(), + getText: () => textContent(editor.state.doc), getEndPosition: () => editor.state.doc.content.size, getSelectedNode: () => editor.state.doc.nodeAt(editor.state.selection.$anchor.pos), getAdjacentNodes: (pos: number) => getAdjacentNodes(editor, pos), diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.scss b/frontend/src/scenes/notebooks/Notebook/Notebook.scss index 589e733a028c2..059b06a20d10a 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.scss +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.scss @@ -32,7 +32,15 @@ height: 0; } - > ul, + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + } + + ul, ol { padding-left: 1rem; @@ -40,11 +48,11 @@ p { margin-bottom: 0.2rem; } - } - } - > ul { - list-style: initial; + > p { + display: inline-block; + } + } } > pre { @@ -117,7 +125,6 @@ .NotebookSidebar { position: relative; width: 0px; - margin-top: 3.6rem; // Account for title transition: width var(--notebook-popover-transition-properties); .NotebookSidebar__content { @@ -139,13 +146,9 @@ } .NotebookNodeSettings__widgets { - position: sticky; - align-self: flex-start; - top: 65px; - &__content { max-height: calc(100vh - 220px); - overflow: scroll; + overflow: auto; } } diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx index b19845fe0b917..ecceb26e1ec93 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx @@ -5,6 +5,193 @@ import { router } from 'kea-router' import { urls } from 'scenes/urls' import { App } from 'scenes/App' import notebook12345Json from './__mocks__/notebook-12345.json' +import { notebookTestTemplate } from './__mocks__/notebook-template-for-snapshot' +import { NotebookType } from '~/types' + +// a list of test cases to run, showing different types of content in notebooks +const testCases: Record = { + 'api/projects/:team_id/notebooks/text-formats': notebookTestTemplate('text-formats', [ + { + type: 'paragraph', + content: [ + { + type: 'text', + marks: [ + { + type: 'bold', + }, + ], + text: ' bold ', + }, + ], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + marks: [ + { + type: 'italic', + }, + ], + text: 'italic', + }, + ], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + marks: [ + { + type: 'bold', + }, + { + type: 'italic', + }, + ], + text: 'bold _and_ italic', + }, + ], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + marks: [ + { + type: 'code', + }, + ], + text: 'code', + }, + ], + }, + ]), + 'api/projects/:team_id/notebooks/headings': notebookTestTemplate('headings', [ + { + type: 'heading', + attrs: { + level: 1, + }, + content: [ + { + type: 'text', + text: 'Heading 1', + }, + ], + }, + { + type: 'heading', + attrs: { + level: 2, + }, + content: [ + { + type: 'text', + text: 'Heading 2', + }, + ], + }, + { + type: 'heading', + attrs: { + level: 3, + }, + content: [ + { + type: 'text', + text: 'Heading 3', + }, + ], + }, + ]), + 'api/projects/:team_id/notebooks/numbered-list': notebookTestTemplate('numbered-list', [ + { + type: 'orderedList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'first item', + }, + ], + }, + ], + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'second item', + }, + ], + }, + ], + }, + ], + }, + ]), + 'api/projects/:team_id/notebooks/bullet-list': notebookTestTemplate('bullet-list', [ + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'first item', + }, + ], + }, + ], + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'second item', + }, + ], + }, + ], + }, + ], + }, + ]), + 'api/projects/:team_id/notebooks/recordings-playlist': notebookTestTemplate('recordings-playlist', [ + { + type: 'ph-recording-playlist', + attrs: { + height: null, + title: 'Session replays', + nodeId: '41faad12-499f-4a4b-95f7-3a36601317cc', + filters: + '{"session_recording_duration":{"type":"recording","key":"duration","value":3600,"operator":"gt"},"properties":[],"events":[],"actions":[],"date_from":"-7d","date_to":null}', + }, + }, + ]), +} const meta: Meta = { title: 'Scenes-App/Notebooks', @@ -15,6 +202,25 @@ const meta: Meta = { }, decorators: [ mswDecorator({ + post: { + 'api/projects/:team_id/query': { + clickhouse: + "SELECT nullIf(nullIf(events.`$session_id`, ''), 'null') AS session_id, any(events.properties) AS properties FROM events WHERE and(equals(events.team_id, 1), in(events.event, [%(hogql_val_0)s, %(hogql_val_1)s]), ifNull(in(session_id, [%(hogql_val_2)s]), 0), ifNull(greaterOrEquals(toTimeZone(events.timestamp, %(hogql_val_3)s), %(hogql_val_4)s), 0), ifNull(lessOrEquals(toTimeZone(events.timestamp, %(hogql_val_5)s), %(hogql_val_6)s), 0)) GROUP BY session_id LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=True", + columns: ['session_id', 'properties'], + hogql: "SELECT properties.$session_id AS session_id, any(properties) AS properties FROM events WHERE and(in(event, ['$pageview', '$autocapture']), in(session_id, ['018a8a51-a39d-7b18-897f-94054eec5f61']), greaterOrEquals(timestamp, '2023-09-11 16:55:36'), lessOrEquals(timestamp, '2023-09-13 18:07:40')) GROUP BY session_id LIMIT 100", + query: "SELECT properties.$session_id as session_id, any(properties) as properties\n FROM events\n WHERE event IN ['$pageview', '$autocapture']\n AND session_id IN ['018a8a51-a39d-7b18-897f-94054eec5f61']\n -- the timestamp range here is only to avoid querying too much of the events table\n -- we don't really care about the absolute value, \n -- but we do care about whether timezones have an odd impact\n -- so, we extend the range by a day on each side so that timezones don't cause issues\n AND timestamp >= '2023-09-11 16:55:36'\n AND timestamp <= '2023-09-13 18:07:40'\n GROUP BY session_id", + results: [ + [ + '018a8a51-a39d-7b18-897f-94054eec5f61', + '{"$os":"Mac OS X","$os_version":"10.15.7","$browser":"Chrome","$device_type":"Desktop","$current_url":"http://localhost:8000/ingestion/platform","$host":"localhost:8000","$pathname":"/ingestion/platform","$browser_version":116,"$browser_language":"en-GB","$screen_height":982,"$screen_width":1512,"$viewport_height":827,"$viewport_width":1498,"$lib":"web","$lib_version":"1.78.2","$insert_id":"249xj40dkv7x9knp","$time":1694537723.201,"distinct_id":"uLI7S0z6rWQIKAjgXhdUBplxPYymuQqxH5QbJKe2wqr","$device_id":"018a8a51-a39c-78f9-a4e4-1183f059f7cc","$user_id":"uLI7S0z6rWQIKAjgXhdUBplxPYymuQqxH5QbJKe2wqr","is_demo_project":false,"$groups":{"project":"018a8a51-9ee3-0000-0369-ff1924dcba89","organization":"018a8a51-988e-0000-d3e6-477c7cc111f1","instance":"http://localhost:8000"},"$autocapture_disabled_server_side":false,"$active_feature_flags":[],"$feature_flag_payloads":{},"realm":"hosted-clickhouse","email_service_available":false,"slack_service_available":false,"$referrer":"http://localhost:8000/signup","$referring_domain":"localhost:8000","$event_type":"click","$ce_version":1,"token":"phc_awewGgfgakHbaSbprHllKajqoa6iP2nz7OAUou763ie","$session_id":"018a8a51-a39d-7b18-897f-94054eec5f61","$window_id":"018a8a51-a39d-7b18-897f-940673bea28c","$set_once":{"$initial_os":"Mac OS X","$initial_browser":"Chrome","$initial_device_type":"Desktop","$initial_current_url":"http://localhost:8000/ingestion/platform","$initial_pathname":"/ingestion/platform","$initial_browser_version":116,"$initial_referrer":"http://localhost:8000/signup","$initial_referring_domain":"localhost:8000"},"$sent_at":"2023-09-12T16:55:23.743000+00:00","$ip":"127.0.0.1","$group_0":"018a8a51-9ee3-0000-0369-ff1924dcba89","$group_1":"018a8a51-988e-0000-d3e6-477c7cc111f1","$group_2":"http://localhost:8000"}', + ], + ], + types: [ + ['session_id', 'Nullable(String)'], + ['properties', 'String'], + ], + }, + }, get: { 'api/projects/:team_id/notebooks': { count: 1, @@ -66,6 +272,76 @@ const meta: Meta = { ], }, 'api/projects/:team_id/notebooks/12345': notebook12345Json, + 'api/projects/:team_id/session_recordings': { + results: [ + { + id: '018a8a51-a39d-7b18-897f-94054eec5f61', + distinct_id: 'uLI7S0z6rWQIKAjgXhdUBplxPYymuQqxH5QbJKe2wqr', + viewed: true, + recording_duration: 4324, + active_seconds: 21, + inactive_seconds: 4302, + start_time: '2023-09-12T16:55:36.404000Z', + end_time: '2023-09-12T18:07:40.147000Z', + click_count: 3, + keypress_count: 0, + mouse_activity_count: 924, + console_log_count: 37, + console_warn_count: 7, + console_error_count: 9, + start_url: 'http://localhost:8000/replay/recent', + person: { + id: 1, + name: 'paul@posthog.com', + distinct_ids: [ + 'uLI7S0z6rWQIKAjgXhdUBplxPYymuQqxH5QbJKe2wqr', + '018a8a51-a39c-78f9-a4e4-1183f059f7cc', + ], + properties: { + email: 'paul@posthog.com', + $initial_os: 'Mac OS X', + $geoip_latitude: -33.8715, + $geoip_city_name: 'Sydney', + $geoip_longitude: 151.2006, + $geoip_time_zone: 'Australia/Sydney', + $initial_browser: 'Chrome', + $initial_pathname: '/', + $initial_referrer: 'http://localhost:8000/signup', + $geoip_postal_code: '2000', + $creator_event_uuid: '018a8a51-a39d-7b18-897f-9407e795547b', + $geoip_country_code: 'AU', + $geoip_country_name: 'Australia', + $initial_current_url: 'http://localhost:8000/', + $initial_device_type: 'Desktop', + $geoip_continent_code: 'OC', + $geoip_continent_name: 'Oceania', + $initial_geoip_latitude: -33.8715, + $initial_browser_version: 116, + $initial_geoip_city_name: 'Sydney', + $initial_geoip_longitude: 151.2006, + $initial_geoip_time_zone: 'Australia/Sydney', + $geoip_subdivision_1_code: 'NSW', + $geoip_subdivision_1_name: 'New South Wales', + $initial_referring_domain: 'localhost:8000', + $initial_geoip_postal_code: '2000', + $initial_geoip_country_code: 'AU', + $initial_geoip_country_name: 'Australia', + $initial_geoip_continent_code: 'OC', + $initial_geoip_continent_name: 'Oceania', + $initial_geoip_subdivision_1_code: 'NSW', + $initial_geoip_subdivision_1_name: 'New South Wales', + }, + created_at: '2023-09-12T16:55:20.736000Z', + uuid: '018a8a51-a3d3-0000-e8fa-94621f9ddd48', + }, + storage: 'clickhouse', + pinned_count: 0, + }, + ], + has_next: false, + version: 3, + }, + ...testCases, }, }), ], @@ -78,6 +354,41 @@ export function NotebooksList(): JSX.Element { return } +export function Headings(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('headings')) + }, []) + return +} + +export function TextFormats(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('text-formats')) + }, []) + return +} + +export function NumberedList(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('numbered-list')) + }, []) + return +} + +export function BulletList(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('bullet-list')) + }, []) + return +} + +export function RecordingsPlaylist(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('recordings-playlist')) + }, []) + return +} + export function TextOnlyNotebook(): JSX.Element { useEffect(() => { router.actions.push(urls.notebook('12345')) diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.tsx b/frontend/src/scenes/notebooks/Notebook/Notebook.tsx index 296301a7d8f7c..6e9adb4825f7b 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.tsx @@ -13,8 +13,6 @@ import { NotebookConflictWarning } from './NotebookConflictWarning' import { NotebookLoadingState } from './NotebookLoadingState' import { Editor } from './Editor' import { EditorFocusPosition } from './utils' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' -import { FEATURE_FLAGS } from 'lib/constants' import { NotebookSidebar } from './NotebookSidebar' import { ErrorBoundary } from '~/layout/ErrorBoundary' @@ -99,9 +97,7 @@ export function Notebook({ shortId, editable = false, initialAutofocus = null }: ) : null}
- - - + { const { selectedNodeLogic, isShowingSidebar, isEditable } = useValues(notebookLogic) + const { setIsShowingSidebar } = useActions(notebookLogic) if (!isEditable) { return null @@ -17,23 +18,29 @@ export const NotebookSidebar = (): JSX.Element | null => { 'NotebookSidebar--showing': isShowingSidebar, })} > -
{selectedNodeLogic && }
+
+ {selectedNodeLogic && isShowingSidebar && ( + setIsShowingSidebar(false)} /> + )} +
) } -export const Widgets = ({ logic }: { logic: BuiltLogic }): JSX.Element | null => { - const { widgets, nodeAttributes, isShowingWidgets } = useValues(logic) - const { updateAttributes, setWidgetsVisible } = useActions(logic) - - if (!isShowingWidgets) { - return null - } +export const Widgets = ({ + logic, + onClose, +}: { + logic: BuiltLogic + onClose: () => void +}): JSX.Element | null => { + const { widgets, nodeAttributes } = useValues(logic) + const { updateAttributes } = useActions(logic) return (
{widgets.map(({ key, label, Component }) => ( - setWidgetsVisible(false)}> +
diff --git a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx index 5ecd3b7951a2b..87d5ee8c1e5c2 100644 --- a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx +++ b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx @@ -3,7 +3,19 @@ import Suggestion from '@tiptap/suggestion' import { ReactRenderer } from '@tiptap/react' import { LemonButton, LemonDivider, lemonToast } from '@posthog/lemon-ui' -import { IconCohort, IconQueryEditor, IconRecording, IconTableChart, IconUploadFile } from 'lib/lemon-ui/icons' +import { + IconCohort, + IconRecording, + IconTableChart, + IconUploadFile, + InsightSQLIcon, + InsightsFunnelsIcon, + InsightsLifecycleIcon, + InsightsPathsIcon, + InsightsRetentionIcon, + InsightsStickinessIcon, + InsightsTrendsIcon, +} from 'lib/lemon-ui/icons' import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' import { EditorCommands, EditorRange } from './utils' import { NotebookNodeType } from '~/types' @@ -57,10 +69,179 @@ const TEXT_CONTROLS: SlashCommandsItem[] = [ ] const SLASH_COMMANDS: SlashCommandsItem[] = [ + { + title: 'Trend', + search: 'trend insight', + icon: , + command: (chain) => + chain.insertContent({ + type: NotebookNodeType.Query, + attrs: { + query: { + kind: 'InsightVizNode', + source: { + kind: 'TrendsQuery', + filterTestAccounts: false, + series: [ + { + kind: 'EventsNode', + event: '$pageview', + name: '$pageview', + math: 'total', + }, + ], + interval: 'day', + trendsFilter: { + display: 'ActionsLineGraph', + }, + }, + }, + }, + }), + }, + { + title: 'Funnel', + search: 'funnel insight', + icon: , + command: (chain) => + chain.insertContent({ + type: NotebookNodeType.Query, + attrs: { + query: { + kind: 'InsightVizNode', + source: { + kind: 'FunnelsQuery', + series: [ + { + kind: 'EventsNode', + name: '$pageview', + event: '$pageview', + }, + { + kind: 'EventsNode', + name: '$pageview', + event: '$pageview', + }, + ], + funnelsFilter: { + funnel_viz_type: 'steps', + }, + }, + }, + }, + }), + }, + { + title: 'Retention', + search: 'retention insight', + icon: , + command: (chain) => + chain.insertContent({ + type: NotebookNodeType.Query, + attrs: { + query: { + kind: 'InsightVizNode', + source: { + kind: 'RetentionQuery', + retentionFilter: { + period: 'Day', + total_intervals: 11, + target_entity: { + id: '$pageview', + name: '$pageview', + type: 'events', + }, + returning_entity: { + id: '$pageview', + name: '$pageview', + type: 'events', + }, + retention_type: 'retention_first_time', + }, + }, + }, + }, + }), + }, + { + title: 'Paths', + search: 'paths insight', + icon: , + command: (chain) => + chain.insertContent({ + type: NotebookNodeType.Query, + attrs: { + query: { + kind: 'InsightVizNode', + source: { + kind: 'PathsQuery', + pathsFilter: { + include_event_types: ['$pageview'], + }, + }, + }, + }, + }), + }, + { + title: 'Stickiness', + search: 'stickiness insight', + icon: , + command: (chain) => + chain.insertContent({ + type: NotebookNodeType.Query, + attrs: { + query: { + kind: 'InsightVizNode', + source: { + kind: 'StickinessQuery', + series: [ + { + kind: 'EventsNode', + name: '$pageview', + event: '$pageview', + math: 'total', + }, + ], + stickinessFilter: {}, + }, + }, + }, + }), + }, + { + title: 'Lifecycle', + search: 'lifecycle insight', + icon: , + command: (chain) => + chain.insertContent({ + type: NotebookNodeType.Query, + attrs: { + query: { + kind: 'InsightVizNode', + source: { + kind: 'LifecycleQuery', + series: [ + { + kind: 'EventsNode', + name: '$pageview', + event: '$pageview', + math: 'total', + }, + ], + lifecycleFilter: { + shown_as: 'Lifecycle', + }, + }, + full: true, + }, + }, + }), + }, { title: 'HogQL', search: 'sql', - icon: , + icon: , command: (chain) => chain.insertContent({ type: NotebookNodeType.Query, attrs: { query: examples['HogQLTable'] } }), }, diff --git a/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts b/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts new file mode 100644 index 0000000000000..b87917836a5db --- /dev/null +++ b/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts @@ -0,0 +1,34 @@ +import { NotebookType } from '~/types' +import { MOCK_DEFAULT_BASIC_USER } from 'lib/api.mock' +import { JSONContent } from 'scenes/notebooks/Notebook/utils' + +export const notebookTestTemplate = ( + title: string = 'Notebook for snapshots', + notebookJson: JSONContent[] +): NotebookType => ({ + short_id: 'template-introduction', + title: title, + created_at: '2023-06-02T00:00:00Z', + last_modified_at: '2023-06-02T00:00:00Z', + created_by: MOCK_DEFAULT_BASIC_USER, + last_modified_by: MOCK_DEFAULT_BASIC_USER, + version: 1, + content: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { + level: 1, + }, + content: [ + { + type: 'text', + text: title, + }, + ], + }, + ...notebookJson, + ], + }, +}) diff --git a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts index 84c8efa165372..68cb94dbc3265 100644 --- a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts +++ b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts @@ -77,6 +77,7 @@ export const notebookLogic = kea([ exportJSON: true, showConflictWarning: true, onUpdateEditor: true, + setIsShowingSidebar: (showing: boolean) => ({ showing }), registerNodeLogic: (nodeLogic: BuiltLogic) => ({ nodeLogic }), unregisterNodeLogic: (nodeLogic: BuiltLogic) => ({ nodeLogic }), setEditable: (editable: boolean) => ({ editable }), @@ -166,6 +167,13 @@ export const notebookLogic = kea([ setEditable: (_, { editable }) => editable, }, ], + isShowingSidebar: [ + false, + { + setSelectedNodeId: (showing, { selectedNodeId }) => (selectedNodeId ? showing : false), + setIsShowingSidebar: (_, { showing }) => showing, + }, + ], }), loaders(({ values, props, actions }) => ({ notebook: [ @@ -178,6 +186,7 @@ export const notebookLogic = kea([ response = { ...values.scratchpadNotebook, content: {}, + text_content: null, version: 0, } } else if (props.shortId.startsWith('template-')) { @@ -210,6 +219,7 @@ export const notebookLogic = kea([ const response = await api.notebooks.update(values.notebook.short_id, { version: values.notebook.version, content: notebook.content, + text_content: values.editor?.getText() || '', title: notebook.title, }) @@ -242,6 +252,7 @@ export const notebookLogic = kea([ // We use the local content if set otherwise the notebook content. That way it supports templates, scratchpad etc. const response = await api.notebooks.create({ content: values.content || values.notebook.content, + text_content: values.editor?.getText() || '', title: values.title || values.notebook.title, }) @@ -331,10 +342,6 @@ export const notebookLogic = kea([ } }, ], - isShowingSidebar: [ - (s) => [s.selectedNodeLogic], - (selectedNodeLogic) => selectedNodeLogic?.values.isShowingWidgets, - ], }), sharedListeners(({ values, actions }) => ({ onNotebookChange: () => { @@ -430,6 +437,7 @@ export const notebookLogic = kea([ return } const jsonContent = values.editor.getJSON() + actions.setLocalContent(jsonContent) actions.onUpdateEditor() }, diff --git a/frontend/src/scenes/notebooks/Notebook/utils.ts b/frontend/src/scenes/notebooks/Notebook/utils.ts index 6947a4ef4a186..ed78f61d20f89 100644 --- a/frontend/src/scenes/notebooks/Notebook/utils.ts +++ b/frontend/src/scenes/notebooks/Notebook/utils.ts @@ -6,6 +6,7 @@ import { getText, JSONContent as TTJSONContent, Range as EditorRange, + TextSerializer, } from '@tiptap/core' import { Node as PMNode } from '@tiptap/pm/model' import { NodeViewProps } from '@tiptap/react' @@ -47,13 +48,13 @@ export type NotebookNodeViewProps = Omit export type NotebookNodeWidget = { key: string label: string - icon: JSX.Element - // using 'any' here shouldn't be necessary but I couldn't figure out how to set a generic on the notebookNodeLogic props + // using 'any' here shouldn't be necessary but, I couldn't figure out how to set a generic on the notebookNodeLogic props Component: ({ attributes, updateAttributes }: NotebookNodeAttributeProperties) => JSX.Element } export interface NotebookEditor { getJSON: () => JSONContent + getText: () => string getEndPosition: () => number getSelectedNode: () => Node | null getAdjacentNodes: (pos: number) => { previous: Node | null; next: Node | null } @@ -88,12 +89,39 @@ export const isCurrentNodeEmpty = (editor: TTEditor): boolean => { return false } -const textContent = (node: any): string => { +export const textContent = (node: any): string => { + // we've extended the node schema to support a custom serializedText function + // each custom node type needs to implement this function, or have an alternative in the map below + const customOrTitleSerializer: TextSerializer = (props): string => { + // TipTap chooses whether to add a separator based on a couple of factors + // but, we always want a separator since this text is for search purposes + const serializedText = props.node.type.spec.serializedText(props.node.attrs) || props.node.attrs?.title || '' + if (serializedText.length > 0 && serializedText[serializedText.length - 1] !== '\n') { + return serializedText + '\n' + } + return serializedText + } + + // we want the type system to complain if we forget to add a custom serializer + const customNodeTextSerializers: Record = { + 'ph-backlink': customOrTitleSerializer, + 'ph-early-access-feature': customOrTitleSerializer, + 'ph-experiment': customOrTitleSerializer, + 'ph-feature-flag': customOrTitleSerializer, + 'ph-feature-flag-code-example': customOrTitleSerializer, + 'ph-image': customOrTitleSerializer, + 'ph-insight': customOrTitleSerializer, + 'ph-person': customOrTitleSerializer, + 'ph-query': customOrTitleSerializer, + 'ph-recording': customOrTitleSerializer, + 'ph-recording-playlist': customOrTitleSerializer, + 'ph-replay-timestamp': customOrTitleSerializer, + 'ph-survey': customOrTitleSerializer, + } + return getText(node, { - blockSeparator: ' ', - textSerializers: { - [NotebookNodeType.ReplayTimestamp]: ({ node }) => `${node.attrs.playbackTime || '00:00'}: `, - }, + blockSeparator: '\n', + textSerializers: customNodeTextSerializers, }) } diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx index dd19fe5216d5c..bd28c9dbe4596 100644 --- a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx +++ b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx @@ -81,12 +81,10 @@ export function NotebookSelectList(props: NotebookSelectProps): JSX.Element { const openNewNotebook = (): void => { const title = newNotebookTitle ?? `Notes ${dayjs().format('DD/MM')}` - if (resource) { - createNotebook(title, NotebookTarget.Popover, [resource], (theNotebookLogic) => { - props.onNotebookOpened?.(theNotebookLogic) - loadNotebooksContainingResource() - }) - } + createNotebook(title, NotebookTarget.Popover, resource ? [resource] : undefined, (theNotebookLogic) => { + props.onNotebookOpened?.(theNotebookLogic) + loadNotebooksContainingResource() + }) setShowPopover(false) } diff --git a/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx b/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx index 5e58920b88617..2ce18eba28801 100644 --- a/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx +++ b/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx @@ -103,6 +103,7 @@ export function NotebooksTable(): JSX.Element { setFilters({ search: s }) }} value={filters.search} + data-attr={'notebooks-search'} />
@@ -127,7 +128,7 @@ export function NotebooksTable(): JSX.Element {
([ }), connect({ values: [notebooksModel, ['notebookTemplates']], + actions: [notebooksModel, ['deleteNotebookSuccess']], }), reducers({ filters: [ @@ -66,6 +67,10 @@ export const notebooksTableLogic = kea([ setFilters: () => { actions.loadNotebooks() }, + deleteNotebookSuccess: () => { + // TODO at some point this will be slow enough it makes sense to patch the in-memory list but for simplicity... + actions.loadNotebooks() + }, })), selectors({ notebooksAndTemplates: [ diff --git a/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx b/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx index e5b0f48b5959c..75045530c1298 100644 --- a/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx +++ b/frontend/src/scenes/organization/ConfirmOrganization/ConfirmOrganization.tsx @@ -10,9 +10,7 @@ import { AnimatedCollapsible } from 'lib/components/AnimatedCollapsible' import { Form } from 'kea-forms' import { BridgePage } from 'lib/components/BridgePage/BridgePage' import SignupRoleSelect from 'lib/components/SignupRoleSelect' -import SignupReferralSourceSelect from 'lib/components/SignupReferralSourceSelect' -import { FEATURE_FLAGS } from 'lib/constants' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import SignupReferralSource from 'lib/components/SignupReferralSource' export const scene: SceneExport = { component: ConfirmOrganization, @@ -22,7 +20,6 @@ export const scene: SceneExport = { export function ConfirmOrganization(): JSX.Element { const { isConfirmOrganizationSubmitting, email, showNewOrgWarning } = useValues(confirmOrganizationLogic) const { setShowNewOrgWarning } = useActions(confirmOrganizationLogic) - const { featureFlags } = useValues(featureFlagLogic) return ( @@ -80,20 +77,7 @@ export function ConfirmOrganization(): JSX.Element { - {featureFlags[FEATURE_FLAGS.REFERRAL_SOURCE_SELECT] === 'test' ? ( - - ) : ( - <> - - - - - )} + ( - {fieldConfig.markdown && ( - - )} + {fieldConfig.markdown && {fieldConfig.markdown}} {fieldConfig.type && isValidField(fieldConfig) ? (